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 @@
"Backbone": true,
"Modernizr": true,
"Popper": true,
"Sortable": true,
"CKEDITOR": true
},
"rules": {
......
This diff is collapsed.
......@@ -845,6 +845,7 @@ jquery.ui.sortable:
- core/jquery.ui
- core/jquery.ui.mouse
- 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:
version: *jquery_ui_version
......@@ -900,6 +901,7 @@ jquery.ui.touch-punch:
- core/jquery.ui
- core/jquery.ui.mouse
- 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:
version: *jquery_ui_version
......@@ -974,6 +976,16 @@ popperjs:
js:
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:
remote: https://github.com/jashkenas/underscore
version: "1.8.3"
......
......@@ -52,13 +52,11 @@ drupal.ckeditor.admin:
- core/drupal
- core/drupalSettings
- core/jquery.once
- core/jquery.ui.sortable
- core/jquery.ui.draggable
- core/jquery.ui.touch-punch
- core/backbone
- core/drupal.dialog
- core/drupal.announce
- core/ckeditor
- core/sortable
- editor/drupal.editor.admin
# Ensure to run after core/drupal.vertical-tabs.
- core/drupal.vertical-tabs
......
......@@ -228,6 +228,7 @@
border-bottom-left-radius: 2px;
}
.ckeditor-button-placeholder,
.ckeditor-buttons .ckeditor-button-placeholder a,
.ckeditor-toolbar-group-placeholder {
background: #9dcae7;
}
......
......@@ -198,11 +198,6 @@
if (!result && $originalGroup) {
$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
// known place.
$target.trigger('focus');
......
......@@ -89,8 +89,6 @@
Drupal.ckeditor.registerButtonMove(this, $button, function (result) {
if (!result && $originalGroup) {
$originalGroup.find('.ckeditor-buttons').append($button);
} else {
view.$el.find('.ui-sortable').sortable('refresh');
}
$target.trigger('focus');
......
......@@ -4,7 +4,7 @@
* configuration.
*/
(function(Drupal, Backbone, $) {
(function(Drupal, Backbone, $, Sortable) {
Drupal.ckeditor.VisualView = Backbone.View.extend(
/** @lends Drupal.ckeditor.VisualView# */ {
events: {
......@@ -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.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
*/
endGroupDrag(event, ui) {
const view = this;
Drupal.ckeditor.registerGroupMove(this, ui.item, success => {
if (!success) {
// Cancel any sorting in the configuration area.
view.$el
.find('.ckeditor-toolbar-configuration')
.find('.ui-sortable')
.sortable('cancel');
}
});
endGroupDrag(event) {
const $item = $(event.item);
Drupal.ckeditor.registerGroupMove(this, $item);
},
/**
* Handles jQuery Sortable start sort of a button.
* Handles Sortable start sort of a button.
*
* @param {jQuery.Event} event
* 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.
* @param {CustomEvent} event
* The event triggered on the button drag.
*/
startButtonDrag(event, ui) {
startButtonDrag(event) {
this.$el.find('a:focus').trigger('blur');
// Show the button group names as soon as the user starts dragging.
......@@ -188,67 +174,70 @@
},
/**
* 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.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
*/
endButtonDrag(event, ui) {
const view = this;
Drupal.ckeditor.registerButtonMove(this, ui.item, success => {
if (!success) {
// Cancel any sorting in the configuration area.
view.$el.find('.ui-sortable').sortable('cancel');
}
// Refocus the target button so that the user can continue from a known
// place.
ui.item.find('a').trigger('focus');
endButtonDrag(event) {
const $item = $(event.item);
Drupal.ckeditor.registerButtonMove(this, $item, success => {
// Refocus the target button so that the user can continue
// from a known place.
$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() {
// Make the buttons sortable.
this.$el
.find('.ckeditor-buttons')
.not('.ui-sortable')
.sortable({
// Change this to .ckeditor-toolbar-group-buttons.
connectWith: '.ckeditor-buttons',
placeholder: 'ckeditor-button-placeholder',
forcePlaceholderSize: true,
tolerance: 'pointer',
cursor: 'move',
start: this.startButtonDrag.bind(this),
// Sorting within a sortable.
stop: this.endButtonDrag.bind(this),
})
.disableSelection();
Array.prototype.forEach.call(
this.el.querySelectorAll('.ckeditor-buttons:not(.js-sortable)'),
buttons => {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
ghostClass: 'ckeditor-button-placeholder',
group: 'ckeditor-buttons',
onStart: this.startButtonDrag.bind(this),
onEnd: this.endButtonDrag.bind(this),
});
},
);
// Add the drag and drop functionality to button groups.
this.$el
.find('.ckeditor-toolbar-groups')
.not('.ui-sortable')
.sortable({
connectWith: '.ckeditor-toolbar-groups',
cancel: '.ckeditor-add-new-group',
placeholder: 'ckeditor-toolbar-group-placeholder',
forcePlaceholderSize: true,
cursor: 'move',
stop: this.endGroupDrag.bind(this),
Array.prototype.forEach.call(
this.el.querySelectorAll(
'.ckeditor-toolbar-groups:not(.js-sortable)',
),
buttons => {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
ghostClass: 'ckeditor-toolbar-group-placeholder',
onEnd: this.endGroupDrag.bind(this),
});
},
);
// Add the drag and drop functionality to buttons.
this.$el.find('.ckeditor-multiple-buttons li').draggable({
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
helper: 'clone',
Array.prototype.forEach.call(
this.el.querySelectorAll(
'.ckeditor-multiple-buttons:not(.js-sortable)',
),
buttons => {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
group: {
name: 'ckeditor-buttons',
pull: 'clone',
},
onEnd: this.endButtonDrag.bind(this),
});
},
);
},
/**
* Wraps the invocation of methods to insert blank groups and rows.
......@@ -312,4 +301,4 @@
},
},
);
})(Drupal, Backbone, jQuery);
})(Drupal, Backbone, jQuery, Sortable);
......@@ -5,7 +5,7 @@
* @preserve
**/
(function (Drupal, Backbone, $) {
(function (Drupal, Backbone, $, Sortable) {
Drupal.ckeditor.VisualView = Backbone.View.extend({
events: {
'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
......@@ -59,53 +59,52 @@
event.preventDefault();
},
endGroupDrag: function endGroupDrag(event, ui) {
var view = this;
Drupal.ckeditor.registerGroupMove(this, ui.item, function (success) {
if (!success) {
view.$el.find('.ckeditor-toolbar-configuration').find('.ui-sortable').sortable('cancel');
}
});
endGroupDrag: function endGroupDrag(event) {
var $item = $(event.item);
Drupal.ckeditor.registerGroupMove(this, $item);
},
startButtonDrag: function startButtonDrag(event, ui) {
startButtonDrag: function startButtonDrag(event) {
this.$el.find('a:focus').trigger('blur');
this.model.set('groupNamesVisible', true);
},
endButtonDrag: function endButtonDrag(event, ui) {
var view = this;
Drupal.ckeditor.registerButtonMove(this, ui.item, function (success) {
if (!success) {
view.$el.find('.ui-sortable').sortable('cancel');
}
endButtonDrag: function endButtonDrag(event) {
var $item = $(event.item);
ui.item.find('a').trigger('focus');
Drupal.ckeditor.registerButtonMove(this, $item, function (success) {
$item.find('a').trigger('focus');
});
},
applySorting: function applySorting() {
this.$el.find('.ckeditor-buttons').not('.ui-sortable').sortable({
connectWith: '.ckeditor-buttons',
placeholder: 'ckeditor-button-placeholder',
forcePlaceholderSize: true,
tolerance: 'pointer',
cursor: 'move',
start: this.startButtonDrag.bind(this),
stop: this.endButtonDrag.bind(this)
}).disableSelection();
this.$el.find('.ckeditor-toolbar-groups').not('.ui-sortable').sortable({
connectWith: '.ckeditor-toolbar-groups',
cancel: '.ckeditor-add-new-group',
placeholder: 'ckeditor-toolbar-group-placeholder',
forcePlaceholderSize: true,
cursor: 'move',
stop: this.endGroupDrag.bind(this)
var _this = this;
Array.prototype.forEach.call(this.el.querySelectorAll('.ckeditor-buttons:not(.js-sortable)'), function (buttons) {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
ghostClass: 'ckeditor-button-placeholder',
group: 'ckeditor-buttons',
onStart: _this.startButtonDrag.bind(_this),
onEnd: _this.endButtonDrag.bind(_this)
});
});
this.$el.find('.ckeditor-multiple-buttons li').draggable({
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
helper: 'clone'
Array.prototype.forEach.call(this.el.querySelectorAll('.ckeditor-toolbar-groups:not(.js-sortable)'), function (buttons) {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
ghostClass: 'ckeditor-toolbar-group-placeholder',
onEnd: _this.endGroupDrag.bind(_this)
});
});
Array.prototype.forEach.call(this.el.querySelectorAll('.ckeditor-multiple-buttons:not(.js-sortable)'), function (buttons) {
buttons.classList.add('js-sortable');
Sortable.create(buttons, {
group: {
name: 'ckeditor-buttons',
pull: 'clone'
},
onEnd: _this.endButtonDrag.bind(_this)
});
});
},
insertPlaceholders: function insertPlaceholders() {
......@@ -142,4 +141,4 @@
});
}
});
})(Drupal, Backbone, jQuery);
\ No newline at end of file
})(Drupal, Backbone, jQuery, Sortable);
\ 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 @@
.layout-builder-block {
padding: 1.5em;
cursor: move;
background-color: #fff;
}
.layout-builder-block [tabindex="-1"] {
......
......@@ -3,7 +3,7 @@
* Attaches the behaviors for the Layout Builder module.
*/
(($, Drupal) => {
(($, Drupal, Sortable) => {
const { ajax, behaviors, debounce, announce, formatPlural } = Drupal;
/*
......@@ -101,62 +101,71 @@
};
/**
* Provides the ability to drag blocks to new positions in the layout.
* Callback used in {@link Drupal.behaviors.layoutBuilderBlockDrag}.
*
* @type {Drupal~behavior}
* @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
*
* @prop {Drupal~behaviorAttach} attach
* Attach block drag behavior to the Layout Builder UI.
* @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
*/
behaviors.layoutBuilderBlockDrag = {
attach(context) {
$(context)
.find('.js-layout-builder-region')
.sortable({
items: '> .js-layout-builder-block',
connectWith: '.js-layout-builder-region',
placeholder: 'ui-state-drop',
Drupal.layoutBuilderBlockUpdate = function(item, from, to) {
const $item = $(item);
const $from = $(from);
/**
* Updates the layout with the new position of the block.
*
* @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]) {
const itemRegion = $item.closest('.js-layout-builder-region');
if (to === itemRegion[0]) {
// Find the destination delta.
const deltaTo = ui.item
.closest('[data-layout-delta]')
.data('layout-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 = ui.sender
? ui.sender.closest('[data-layout-delta]').data('layout-delta')
const deltaFrom = $from
? $from.closest('[data-layout-delta]').data('layout-delta')
: deltaTo;
ajax({
url: [
ui.item
.closest('[data-layout-update-url]')
.data('layout-update-url'),
$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'),
$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.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attach block drag behavior to the Layout Builder UI.
*/
behaviors.layoutBuilderBlockDrag = {
attach(context) {
const regionSelector = '.js-layout-builder-region';
Array.prototype.forEach.call(
context.querySelectorAll(regionSelector),
region => {
Sortable.create(region, {
draggable: '.js-layout-builder-block',
ghostClass: 'ui-state-drop',
group: 'builder-region',
onEnd: event =>
Drupal.layoutBuilderBlockUpdate(event.item, event.from, event.to),
});
},
);
},
};
/**
......@@ -441,4 +450,4 @@
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 @@
* @preserve
**/
(function ($, Drupal) {
(function ($, Drupal, Sortable) {
var ajax = Drupal.ajax,
behaviors = Drupal.behaviors,
debounce = Drupal.debounce,
......@@ -53,27 +53,36 @@
}
};
behaviors.layoutBuilderBlockDrag = {
attach: function attach(context) {
$(context).find('.js-layout-builder-region').sortable({
items: '> .js-layout-builder-block',
connectWith: '.js-layout-builder-region',
placeholder: 'ui-state-drop',
Drupal.layoutBuilderBlockUpdate = function (item, from, to) {
var $item = $(item);
var $from = $(from);
update: function update(event, ui) {
var itemRegion = ui.item.closest('.js-layout-builder-region');
if (event.target === itemRegion[0]) {
var deltaTo = ui.item.closest('[data-layout-delta]').data('layout-delta');
var itemRegion = $item.closest('.js-layout-builder-region');
if (to === itemRegion[0]) {
var deltaTo = $item.closest('[data-layout-delta]').data('layout-delta');
var deltaFrom = ui.sender ? ui.sender.closest('[data-layout-delta]').data('layout-delta') : deltaTo;
var deltaFrom = $from ? $from.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(function (element) {
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 = {
attach: function attach(context) {
var regionSelector = '.js-layout-builder-region';
Array.prototype.forEach.call(context.querySelectorAll(regionSelector), function (region) {
Sortable.create(region, {
draggable: '.js-layout-builder-block',
ghostClass: 'ui-state-drop',
group: 'builder-region',
onEnd: function onEnd(event) {
return Drupal.layoutBuilderBlockUpdate(event.item, event.from, event.to);
}
});
});
}
};
......@@ -213,4 +222,4 @@
return '<div class="layout-builder-block__content-preview-placeholder-label js-layout-builder-content-preview-placeholder-label">' + contentPreviewPlaceholderText + '</div>';
};
})(jQuery, Drupal);
\ No newline at end of file
})(jQuery, Drupal, Sortable);
\ No newline at end of file
......@@ -6,7 +6,7 @@ drupal.layout_builder:
js:
js/layout-builder.js: {}
dependencies:
- core/jquery.ui.sortable
- core/sortable
- core/drupal.dialog.off_canvas
- core/drupal.announce
- core/drupal.debounce
......
......@@ -14,6 +14,7 @@
class ContentPreviewToggleTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use LayoutBuilderSortTrait;
/**
* {@inheritdoc}
......@@ -91,9 +92,14 @@ public function testContentPreviewToggle() {
// Confirm repositioning blocks works with content preview disabled.
$this->assertOrderInPage([$links_field_placeholder_label, $body_field_placeholder_label]);
$links_block_placeholder_child = $assert_session->elementExists('css', "[data-layout-content-preview-placeholder-label='$links_field_placeholder_label'] div");
$body_block_placeholder_child = $assert_session->elementExists('css', "[data-layout-content-preview-placeholder-label='$body_field_placeholder_label'] div");
$body_block_placeholder_child->dragTo($links_block_placeholder_child);
$region_content = '.layout__region--content';
$links_block = "[data-layout-content-preview-placeholder-label='$links_field_placeholder_label']";
$body_block = "[data-layout-content-preview-placeholder-label='$body_field_placeholder_label']";
$assert_session->elementExists('css', $links_block . " div");
$assert_session->elementExists('css', $body_block . " div");
$this->sortableAfter($links_block, $body_block, $region_content);
$assert_session->assertWaitOnAjaxRequest();
// Check that the drag-triggered rebuild did not trigger content preview.
......
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\SortableTestTrait;
/**
* LayoutBuilderSortTrait, provides callback for simulated layout change.
*/
trait LayoutBuilderSortTrait {
use SortableTestTrait;
/**
* {@inheritdoc}
*/
protected function sortableUpdate($item, $from, $to = NULL) {
// If container does not change, $from and $to are equal.
$to = $to ?: $from;
$script = <<<JS
(function (src, from, to) {
var sourceElement = document.querySelector(src);
var fromElement = document.querySelector(from);
var toElement = document.querySelector(to);
Drupal.layoutBuilderBlockUpdate(sourceElement, fromElement, toElement)
})('{$item}', '{$from}', '{$to}')
JS;
$options = [
'script' => $script,
'args' => [],
];
$this->getSession()->getDriver()->getWebDriverSession()->execute($options);
}
}
......@@ -16,6 +16,7 @@
class LayoutBuilderTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use LayoutBuilderSortTrait;
/**
* {@inheritdoc}
......@@ -162,12 +163,13 @@ public function testLayoutBuilderUi() {
$assert_session->elementTextNotContains('css', '.layout__region--second', 'Powered by Drupal');
// Drag the block to a region in different section.
<