Commit 18351b7e authored by lauriii's avatar lauriii

Issue #3064049 by zrpnr, lauriii, bnjmnm, finnsky, alexpott, tedbow,...

Issue #3064049 by zrpnr, lauriii, bnjmnm, finnsky, alexpott, tedbow, phenaproxima, Wim Leers, xjm, Berdir, sasanikolic, justafish, larowlan: Replace jQuery UI sortable with Sortable js

(cherry picked from commit 4fb98eb2)
parent 892201cc
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"Backbone": true, "Backbone": true,
"Modernizr": true, "Modernizr": true,
"Popper": true, "Popper": true,
"Sortable": true,
"CKEDITOR": true "CKEDITOR": true
}, },
"rules": { "rules": {
......
This diff is collapsed.
...@@ -845,6 +845,7 @@ jquery.ui.sortable: ...@@ -845,6 +845,7 @@ jquery.ui.sortable:
- core/jquery.ui - core/jquery.ui
- core/jquery.ui.mouse - core/jquery.ui.mouse
- core/jquery.ui.widget - core/jquery.ui.widget
deprecated: The "%library_id%" asset library is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. See https://www.drupal.org/node/3084730
jquery.ui.spinner: jquery.ui.spinner:
version: *jquery_ui_version version: *jquery_ui_version
...@@ -900,6 +901,7 @@ jquery.ui.touch-punch: ...@@ -900,6 +901,7 @@ jquery.ui.touch-punch:
- core/jquery.ui - core/jquery.ui
- core/jquery.ui.mouse - core/jquery.ui.mouse
- core/jquery.ui.widget - core/jquery.ui.widget
deprecated: The "%library_id%" asset library is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. See https://www.drupal.org/node/3084730
jquery.ui.widget: jquery.ui.widget:
version: *jquery_ui_version version: *jquery_ui_version
...@@ -974,6 +976,16 @@ popperjs: ...@@ -974,6 +976,16 @@ popperjs:
js: js:
assets/vendor/popperjs/popper.min.js: { minified: true } assets/vendor/popperjs/popper.min.js: { minified: true }
sortable:
remote: https://github.com/SortableJS/Sortable
version: "1.10.0"
license:
name: MIT
url: https://github.com/SortableJS/Sortable/tree/master#mit-license
gpl-compatible: true
js:
assets/vendor/sortable/Sortable.min.js: { minified: true }
underscore: underscore:
remote: https://github.com/jashkenas/underscore remote: https://github.com/jashkenas/underscore
version: "1.8.3" version: "1.8.3"
......
...@@ -52,13 +52,11 @@ drupal.ckeditor.admin: ...@@ -52,13 +52,11 @@ drupal.ckeditor.admin:
- core/drupal - core/drupal
- core/drupalSettings - core/drupalSettings
- core/jquery.once - core/jquery.once
- core/jquery.ui.sortable
- core/jquery.ui.draggable
- core/jquery.ui.touch-punch
- core/backbone - core/backbone
- core/drupal.dialog - core/drupal.dialog
- core/drupal.announce - core/drupal.announce
- core/ckeditor - core/ckeditor
- core/sortable
- editor/drupal.editor.admin - editor/drupal.editor.admin
# Ensure to run after core/drupal.vertical-tabs. # Ensure to run after core/drupal.vertical-tabs.
- core/drupal.vertical-tabs - core/drupal.vertical-tabs
......
...@@ -228,6 +228,7 @@ ...@@ -228,6 +228,7 @@
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
} }
.ckeditor-button-placeholder, .ckeditor-button-placeholder,
.ckeditor-buttons .ckeditor-button-placeholder a,
.ckeditor-toolbar-group-placeholder { .ckeditor-toolbar-group-placeholder {
background: #9dcae7; background: #9dcae7;
} }
......
...@@ -198,11 +198,6 @@ ...@@ -198,11 +198,6 @@
if (!result && $originalGroup) { if (!result && $originalGroup) {
$originalGroup.find('.ckeditor-buttons').append($button); $originalGroup.find('.ckeditor-buttons').append($button);
} }
// Otherwise refresh the sortables to acknowledge the new button
// positions.
else {
view.$el.find('.ui-sortable').sortable('refresh');
}
// Refocus the target button so that the user can continue from a // Refocus the target button so that the user can continue from a
// known place. // known place.
$target.trigger('focus'); $target.trigger('focus');
......
...@@ -89,9 +89,7 @@ ...@@ -89,9 +89,7 @@
Drupal.ckeditor.registerButtonMove(this, $button, function (result) { Drupal.ckeditor.registerButtonMove(this, $button, function (result) {
if (!result && $originalGroup) { if (!result && $originalGroup) {
$originalGroup.find('.ckeditor-buttons').append($button); $originalGroup.find('.ckeditor-buttons').append($button);
} else { }
view.$el.find('.ui-sortable').sortable('refresh');
}
$target.trigger('focus'); $target.trigger('focus');
}); });
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* configuration. * configuration.
*/ */
(function(Drupal, Backbone, $) { (function(Drupal, Backbone, $, Sortable) {
Drupal.ckeditor.VisualView = Backbone.View.extend( Drupal.ckeditor.VisualView = Backbone.View.extend(
/** @lends Drupal.ckeditor.VisualView# */ { /** @lends Drupal.ckeditor.VisualView# */ {
events: { events: {
...@@ -150,37 +150,23 @@ ...@@ -150,37 +150,23 @@
}, },
/** /**
* Handles jQuery Sortable stop sort of a button group. * Handles Sortable stop sort of a button group.
* *
* @param {jQuery.Event} event * @param {CustomEvent} event
* The event triggered on the group drag. * The event triggered on the group drag.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
*/ */
endGroupDrag(event, ui) { endGroupDrag(event) {
const view = this; const $item = $(event.item);
Drupal.ckeditor.registerGroupMove(this, ui.item, success => { Drupal.ckeditor.registerGroupMove(this, $item);
if (!success) {
// Cancel any sorting in the configuration area.
view.$el
.find('.ckeditor-toolbar-configuration')
.find('.ui-sortable')
.sortable('cancel');
}
});
}, },
/** /**
* Handles jQuery Sortable start sort of a button. * Handles Sortable start sort of a button.
* *
* @param {jQuery.Event} event * @param {CustomEvent} event
* The event triggered on the group drag. * The event triggered on the button drag.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
*/ */
startButtonDrag(event, ui) { startButtonDrag(event) {
this.$el.find('a:focus').trigger('blur'); this.$el.find('a:focus').trigger('blur');
// Show the button group names as soon as the user starts dragging. // Show the button group names as soon as the user starts dragging.
...@@ -188,66 +174,69 @@ ...@@ -188,66 +174,69 @@
}, },
/** /**
* Handles jQuery Sortable stop sort of a button. * Handles Sortable stop sort of a button.
* *
* @param {jQuery.Event} event * @param {CustomEvent} event
* The event triggered on the button drag. * The event triggered on the button drag.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
*/ */
endButtonDrag(event, ui) { endButtonDrag(event) {
const view = this; const $item = $(event.item);
Drupal.ckeditor.registerButtonMove(this, ui.item, success => {
if (!success) { Drupal.ckeditor.registerButtonMove(this, $item, success => {
// Cancel any sorting in the configuration area. // Refocus the target button so that the user can continue
view.$el.find('.ui-sortable').sortable('cancel'); // from a known place.
} $item.find('a').trigger('focus');
// Refocus the target button so that the user can continue from a known
// place.
ui.item.find('a').trigger('focus');
}); });
}, },
/** /**
* Invokes jQuery.sortable() on new buttons and groups in a CKEditor config. * Invokes Sortable() on new buttons and groups in a CKEditor config.
* Array.prototype.forEach is used here because of the lack of support for
* NodeList.forEach in older browsers.
*/ */
applySorting() { applySorting() {
// Make the buttons sortable. // Make the buttons sortable.
this.$el Array.prototype.forEach.call(
.find('.ckeditor-buttons') this.el.querySelectorAll('.ckeditor-buttons:not(.js-sortable)'),
.not('.ui-sortable') buttons => {
.sortable({ buttons.classList.add('js-sortable');
// Change this to .ckeditor-toolbar-group-buttons. Sortable.create(buttons, {
connectWith: '.ckeditor-buttons', ghostClass: 'ckeditor-button-placeholder',
placeholder: 'ckeditor-button-placeholder', group: 'ckeditor-buttons',
forcePlaceholderSize: true, onStart: this.startButtonDrag.bind(this),
tolerance: 'pointer', onEnd: this.endButtonDrag.bind(this),
cursor: 'move', });
start: this.startButtonDrag.bind(this), },
// Sorting within a sortable. );
stop: this.endButtonDrag.bind(this),
})
.disableSelection();
// Add the drag and drop functionality to button groups. Array.prototype.forEach.call(
this.$el this.el.querySelectorAll(
.find('.ckeditor-toolbar-groups') '.ckeditor-toolbar-groups:not(.js-sortable)',
.not('.ui-sortable') ),
.sortable({ buttons => {
connectWith: '.ckeditor-toolbar-groups', buttons.classList.add('js-sortable');
cancel: '.ckeditor-add-new-group', Sortable.create(buttons, {
placeholder: 'ckeditor-toolbar-group-placeholder', ghostClass: 'ckeditor-toolbar-group-placeholder',
forcePlaceholderSize: true, onEnd: this.endGroupDrag.bind(this),
cursor: 'move', });
stop: this.endGroupDrag.bind(this), },
}); );
// Add the drag and drop functionality to buttons. Array.prototype.forEach.call(
this.$el.find('.ckeditor-multiple-buttons li').draggable({ this.el.querySelectorAll(
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons', '.ckeditor-multiple-buttons:not(.js-sortable)',
helper: 'clone', ),
}); buttons => {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
group: {
name: 'ckeditor-buttons',
pull: 'clone',
},
onEnd: this.endButtonDrag.bind(this),
});
},
);
}, },
/** /**
...@@ -312,4 +301,4 @@ ...@@ -312,4 +301,4 @@
}, },
}, },
); );
})(Drupal, Backbone, jQuery); })(Drupal, Backbone, jQuery, Sortable);
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
* @preserve * @preserve
**/ **/
(function (Drupal, Backbone, $) { (function (Drupal, Backbone, $, Sortable) {
Drupal.ckeditor.VisualView = Backbone.View.extend({ Drupal.ckeditor.VisualView = Backbone.View.extend({
events: { events: {
'click .ckeditor-toolbar-group-name': 'onGroupNameClick', 'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
...@@ -59,53 +59,52 @@ ...@@ -59,53 +59,52 @@
event.preventDefault(); event.preventDefault();
}, },
endGroupDrag: function endGroupDrag(event, ui) { endGroupDrag: function endGroupDrag(event) {
var view = this; var $item = $(event.item);
Drupal.ckeditor.registerGroupMove(this, ui.item, function (success) { Drupal.ckeditor.registerGroupMove(this, $item);
if (!success) {
view.$el.find('.ckeditor-toolbar-configuration').find('.ui-sortable').sortable('cancel');
}
});
}, },
startButtonDrag: function startButtonDrag(event, ui) { startButtonDrag: function startButtonDrag(event) {
this.$el.find('a:focus').trigger('blur'); this.$el.find('a:focus').trigger('blur');
this.model.set('groupNamesVisible', true); this.model.set('groupNamesVisible', true);
}, },
endButtonDrag: function endButtonDrag(event, ui) { endButtonDrag: function endButtonDrag(event) {
var view = this; var $item = $(event.item);
Drupal.ckeditor.registerButtonMove(this, ui.item, function (success) {
if (!success) {
view.$el.find('.ui-sortable').sortable('cancel');
}
ui.item.find('a').trigger('focus'); Drupal.ckeditor.registerButtonMove(this, $item, function (success) {
$item.find('a').trigger('focus');
}); });
}, },
applySorting: function applySorting() { applySorting: function applySorting() {
this.$el.find('.ckeditor-buttons').not('.ui-sortable').sortable({ var _this = this;
connectWith: '.ckeditor-buttons',
placeholder: 'ckeditor-button-placeholder', Array.prototype.forEach.call(this.el.querySelectorAll('.ckeditor-buttons:not(.js-sortable)'), function (buttons) {
forcePlaceholderSize: true, buttons.classList.add('js-sortable');
tolerance: 'pointer', Sortable.create(buttons, {
cursor: 'move', ghostClass: 'ckeditor-button-placeholder',
start: this.startButtonDrag.bind(this), group: 'ckeditor-buttons',
onStart: _this.startButtonDrag.bind(_this),
stop: this.endButtonDrag.bind(this) onEnd: _this.endButtonDrag.bind(_this)
}).disableSelection(); });
});
this.$el.find('.ckeditor-toolbar-groups').not('.ui-sortable').sortable({
connectWith: '.ckeditor-toolbar-groups', Array.prototype.forEach.call(this.el.querySelectorAll('.ckeditor-toolbar-groups:not(.js-sortable)'), function (buttons) {
cancel: '.ckeditor-add-new-group', buttons.classList.add('js-sortable');
placeholder: 'ckeditor-toolbar-group-placeholder', Sortable.create(buttons, {
forcePlaceholderSize: true, ghostClass: 'ckeditor-toolbar-group-placeholder',
cursor: 'move', onEnd: _this.endGroupDrag.bind(_this)
stop: this.endGroupDrag.bind(this) });
}); });
this.$el.find('.ckeditor-multiple-buttons li').draggable({ Array.prototype.forEach.call(this.el.querySelectorAll('.ckeditor-multiple-buttons:not(.js-sortable)'), function (buttons) {
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons', buttons.classList.add('js-sortable');
helper: 'clone' Sortable.create(buttons, {
group: {
name: 'ckeditor-buttons',
pull: 'clone'
},
onEnd: _this.endButtonDrag.bind(_this)
});
}); });
}, },
insertPlaceholders: function insertPlaceholders() { insertPlaceholders: function insertPlaceholders() {
...@@ -142,4 +141,4 @@ ...@@ -142,4 +141,4 @@
}); });
} }
}); });
})(Drupal, Backbone, jQuery); })(Drupal, Backbone, jQuery, Sortable);
\ No newline at end of file \ No newline at end of file
<?php
namespace Drupal\Tests\ckeditor\Traits;
use Drupal\FunctionalJavascriptTests\SortableTestTrait;
/**
* Provides callback for simulated CKEditor toolbar configuration change.
*/
trait CKEditorAdminSortTrait {
use SortableTestTrait;
/**
* {@inheritdoc}
*/
protected function sortableUpdate($item, $from, $to = NULL) {
$script = <<<JS
(function () {
// Set backbone model after a DOM change.
Drupal.ckeditor.models.Model.set('isDirty', true);
})()
JS;
$options = [
'script' => $script,
'args' => [],
];
$this->getSession()->getDriver()->getWebDriverSession()->execute($options);
}
}
...@@ -89,6 +89,7 @@ ...@@ -89,6 +89,7 @@
.layout-builder-block { .layout-builder-block {
padding: 1.5em; padding: 1.5em;
cursor: move; cursor: move;
background-color: #fff;
} }
.layout-builder-block [tabindex="-1"] { .layout-builder-block [tabindex="-1"] {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* Attaches the behaviors for the Layout Builder module. * Attaches the behaviors for the Layout Builder module.
*/ */
(($, Drupal) => { (($, Drupal, Sortable) => {
const { ajax, behaviors, debounce, announce, formatPlural } = Drupal; const { ajax, behaviors, debounce, announce, formatPlural } = Drupal;
/* /*
...@@ -100,6 +100,48 @@ ...@@ -100,6 +100,48 @@
}, },
}; };
/**
* Callback used in {@link Drupal.behaviors.layoutBuilderBlockDrag}.
*
* @param {HTMLElement} item
* The HTML element representing the repositioned block.
* @param {HTMLElement} from
* The HTML element representing the previous parent of item
* @param {HTMLElement} to
* The HTML element representing the current parent of item
*
* @internal This method is a callback for layoutBuilderBlockDrag and is used
* in FunctionalJavascript tests. It may be renamed if the test changes.
* @see https://www.drupal.org/node/3084730
*/
Drupal.layoutBuilderBlockUpdate = function(item, from, to) {
const $item = $(item);
const $from = $(from);
// Check if the region from the event and region for the item match.
const itemRegion = $item.closest('.js-layout-builder-region');
if (to === itemRegion[0]) {
// Find the destination delta.
const deltaTo = $item.closest('[data-layout-delta]').data('layout-delta');
// If the block didn't leave the original delta use the destination.
const deltaFrom = $from
? $from.closest('[data-layout-delta]').data('layout-delta')
: deltaTo;
ajax({
url: [
$item.closest('[data-layout-update-url]').data('layout-update-url'),
deltaFrom,
deltaTo,
itemRegion.data('region'),
$item.data('layout-block-uuid'),
$item.prev('[data-layout-block-uuid]').data('layout-block-uuid'),
]
.filter(element => element !== undefined)
.join('/'),
}).execute();
}
};
/** /**
* Provides the ability to drag blocks to new positions in the layout. * Provides the ability to drag blocks to new positions in the layout.
* *
...@@ -110,52 +152,19 @@ ...@@ -110,52 +152,19 @@
*/ */
behaviors.layoutBuilderBlockDrag = { behaviors.layoutBuilderBlockDrag = {
attach(context) { attach(context) {
$(context) const regionSelector = '.js-layout-builder-region';
.find('.js-layout-builder-region') Array.prototype.forEach.call(
.sortable({ context.querySelectorAll(regionSelector),
items: '> .js-layout-builder-block', region => {
connectWith: '.js-layout-builder-region', Sortable.create(region, {
placeholder: 'ui-state-drop', draggable: '.js-layout-builder-block',
ghostClass: 'ui-state-drop',
/** group: 'builder-region',
* Updates the layout with the new position of the block. onEnd: event =>
* Drupal.layoutBuilderBlockUpdate(event.item, event.from, event.to),
* @param {jQuery.Event} event });
* The jQuery Event object. },
* @param {Object} ui );
* An object containing information about the item being sorted.
*/
update(event, ui) {
// Check if the region from the event and region for the item match.
const itemRegion = ui.item.closest('.js-layout-builder-region');
if (event.target === itemRegion[0]) {
// Find the destination delta.
const deltaTo = ui.item
.closest('[data-layout-delta]')
.data('layout-delta');
// If the block didn't leave the original delta use the destination.
const deltaFrom = ui.sender
? ui.sender.closest('[data-layout-delta]').data('layout-delta')
: deltaTo;
ajax({
url: [
ui.item
.closest('[data-layout-update-url]')
.data('layout-update-url'),
deltaFrom,
deltaTo,
itemRegion.data('region'),
ui.item.data('layout-block-uuid'),
ui.item
.prev('[data-layout-block-uuid]')
.data('layout-block-uuid'),
]
.filter(element => element !== undefined)
.join('/'),
}).execute();
}
},
});
}, },
}; };
...@@ -441,4 +450,4 @@ ...@@ -441,4 +450,4 @@
return `<div class="layout-builder-block__content-preview-placeholder-label js-layout-builder-content-preview-placeholder-label">${contentPreviewPlaceholderText}</div>`; return `<div class="layout-builder-block__content-preview-placeholder-label js-layout-builder-content-preview-placeholder-label">${contentPreviewPlaceholderText}</div>`;
}; };
})(jQuery, Drupal); })(jQuery, Drupal, Sortable);
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
* @preserve * @preserve
**/ **/
(function ($, Drupal) { (function ($, Drupal, Sortable) {
var ajax = Drupal.ajax, var ajax = Drupal.ajax,
behaviors = Drupal.behaviors, behaviors = Drupal.behaviors,
debounce = Drupal.debounce, debounce = Drupal.debounce,
...@@ -53,26 +53,35 @@ ...@@ -53,26 +53,35 @@
} }
}; };
Drupal.layoutBuilderBlockUpdate = function (item, from, to) {
var $item = $(item);
var $from = $(from);
var itemRegion = $item.closest('.js-layout-builder-region');
if (to === itemRegion[0]) {
var deltaTo = $item.closest('[data-layout-delta]').data('layout-delta');
var deltaFrom = $from ? $from.closest('[data-layout-delta]').data('layout-delta') : deltaTo;
ajax({
url: [$item.closest('[data-layout-update-url]').data('layout-update-url'), deltaFrom, deltaTo, itemRegion.data('region'), $item.data('layout-block-uuid'), $item.prev('[data-layout-block-uuid]').data('layout-block-uuid')].filter(function (element) {
return element !== undefined;
}).join('/')
}).execute();
}
};
behaviors.layoutBuilderBlockDrag = { behaviors.layoutBuilderBlockDrag = {
attach: function attach(context) { attach: function attach(context) {
$(context).find('.js-layout-builder-region').sortable({ var regionSelector = '.js-layout-builder-region';
items: '> .js-layout-builder-block', Array.prototype.forEach.call(context.querySelectorAll(regionSelector), function (region) {
connectWith: '.js-layout-builder-region', Sortable.create(region, {
placeholder: 'ui-state-drop', draggable: '.js-layout-builder-block',
ghostClass: 'ui-state-drop',
update: function update(event, ui) { group: 'builder-region',
var itemRegion = ui.item.closest('.js-layout-builder-reg