diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js
index 754962b3c7..5b4029386f 100644
--- a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js
+++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js
@@ -289,11 +289,6 @@ function dependencies() {
"./node_modules/@umbraco-ui/uui-css/dist/uui-text.css"
],
"base": "./node_modules/@umbraco-ui"
- },
- {
- "name": "sortablejs",
- "src": ["./node_modules/sortablejs/Sortable.min.js"],
- "base": "./node_modules/sortablejs"
}
];
diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json
index 737b63ab1d..ec7589eca6 100644
--- a/src/Umbraco.Web.UI.Client/package-lock.json
+++ b/src/Umbraco.Web.UI.Client/package-lock.json
@@ -38,7 +38,6 @@
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
"nouislider": "15.6.1",
- "sortablejs": "1.15.0",
"spectrum-colorpicker2": "2.0.9",
"tinymce": "4.9.11",
"typeahead.js": "0.11.1",
@@ -15113,11 +15112,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/sortablejs": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
- "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
- },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -28961,11 +28955,6 @@
"sort-keys": "^1.0.0"
}
},
- "sortablejs": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
- "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
- },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index 8c4e691fcc..8d8334e3a7 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -49,7 +49,6 @@
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
"nouislider": "15.6.1",
- "sortablejs": "1.15.0",
"spectrum-colorpicker2": "2.0.9",
"tinymce": "4.9.11",
"typeahead.js": "0.11.1",
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridinlineblock/gridinlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridinlineblock/gridinlineblock.editor.html
index 3a67a1be88..71c4da4178 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridinlineblock/gridinlineblock.editor.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridinlineblock/gridinlineblock.editor.html
@@ -71,7 +71,11 @@
min-height: 48px;
height: auto;
}
-
+
+ .blockelement-gridinlineblock-editor > slot {
+ --umb-block-grid--inline-editor--pointer-events--condition: var(--umb-block-grid--dragging-mode) none;
+ pointer-events: var(--umb-block-grid--inline-editor--pointer-events--condition, auto);
+ }
@@ -100,6 +104,6 @@
-
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less
index b6205d73ac..c037631d46 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less
@@ -188,7 +188,8 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-grid__block:not(.--acti
&.--active {
/** Avoid displaying hover when dragging-mode */
- --umb-block-grid--block-ui-opacity: calc(1 - var(--umb-block-grid--dragging-mode, 0));
+ --umb-block-grid--block-ui-opacity--condition: var(--umb-block-grid--dragging-mode) 0;
+ --umb-block-grid--block-ui-opacity: var(--umb-block-grid--block-ui-opacity--code, 1);
> .umb-block-grid__block--context {
opacity: var(--umb-block-grid--block-ui-opacity);
@@ -412,7 +413,9 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
z-index: 1; /** overwritten for the first one of an area. */
/** Avoid showing inline-create in dragging-mode */
- opacity: calc(1 - var(--umb-block-grid--dragging-mode, 0));
+
+ --umb-block-grid__block--inline-create-button-display--condition: var(--umb-block-grid--dragging-mode) none;
+ display: var(--umb-block-grid__block--inline-create-button-display--condition);
}
.umb-block-grid__block--inline-create-button.--above {
left: 0;
@@ -420,7 +423,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
top: calc(var(--umb-block-grid--row-gap, 0px) * -0.5);
}
-.umb-block-grid__layout-item:first-of-type .umb-block-grid__block--inline-create-button.--above {
+.umb-block-grid__layout-item:first-of-type > .umb-block-grid__block--inline-create-button.--above {
/* Do not use row-gap if the first one. */
top: 0;
}
@@ -454,6 +457,10 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
/* Move inline create button slightly up, to avoid collision with others*/
margin-bottom: -7px;
margin-top: -13px;
+
+ /** Avoid showing last-inline-create in dragging-mode */
+ --umb-block-grid__block--last-inline-create-button-display--condition: var(--umb-block-grid--dragging-mode) none;
+ display: var(--umb-block-grid__block--last-inline-create-button-display--condition);
}
@@ -553,6 +560,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
.umb-block-grid__area-actions {
grid-column: span var(--umb-block-grid--area-column-span);
+ flex-grow: 1;
opacity: calc(var(--umb-block-grid--hint-area-ui, 0) * .5 + var(--umb-block-grid--show-area-ui, 0));
transition: opacity 120ms;
@@ -590,10 +598,12 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
}
}
+
.umb-block-grid__layout-item-placeholder {
background: transparent;
border-radius: 3px;
+ box-sizing: border-box;
border: solid 1px;
border-color: rgba(@blueDark, .5);
border-radius: 3px;
@@ -601,9 +611,11 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
height: 100%;
}
+
.umb-block-grid__layout-item-placeholder > * {
display: none;
}
+
.umb-block-grid__layout-item-placeholder::before {
content: '';
position:absolute;
@@ -611,6 +623,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
inset: 0;
border-radius: 3px;
background-color: white;
+ pointer-events:none;
}
.umb-block-grid__layout-item-placeholder::after {
content: '';
@@ -618,6 +631,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
z-index:1;
inset: 0;
border-radius: 3px;
+ pointer-events:none;
transition: background-color 240ms ease-in;
animation: umb-block-grid__placeholder__pulse 400ms ease-in-out alternate infinite;
@@ -627,10 +641,14 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
}
}
-
.umb-block-grid__area {
position: relative;
- --umb-block-grid--show-area-ui: 0;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ --umb-block-grid__area--show-area-ui--condition: var(--umb-block-grid--dragging-mode) 1;
+ --umb-block-grid--show-area-ui: var(--umb-block-grid__area--show-area-ui--condition, 0);
}
.umb-block-grid__area:focus,
.umb-block-grid__area:focus-within,
@@ -654,10 +672,9 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
/* Moved slightly in to align with the inline-create button, which is moved slightly in to avoid collision with other create buttons. */
top:2px;
bottom: 2px;
- /** Avoid displaying highlight when in dragging-mode */
- opacity: calc(1 - var(--umb-block-grid--dragging-mode, 0));
border-color: @blueDark;
box-shadow: 0 0 0 1px rgba(255, 255, 255, .7), inset 0 0 0 1px rgba(255, 255, 255, .7);
+
}
.umb-block-grid__area:has( .umb-block-grid__layout-item-placeholder )::after {
opacity: 1;
@@ -674,6 +691,9 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
100% { border-color: rgba(@blueDark, 0.66); }
}
}
+.umb-block-grid__area > ng-form {
+ display: contents;
+}
.umb-block-grid__scalebox-backdrop {
position: absolute;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.area.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.area.overlay.controller.js
index f20f14f227..1c2f7e1c2b 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.area.overlay.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.area.overlay.controller.js
@@ -25,7 +25,7 @@
value: {min:vm.area.minAllowed, max:vm.area.maxAllowed}
}
- unsubscribe.push($scope.$watch('vm.area.alias', (newVal, oldVal) => {
+ unsubscribe.push($scope.$watch('vm.area.alias', (newVal) => {
$scope.model.updateTitle();
if($scope.blockGridBlockConfigurationAreaForm.alias) {
$scope.blockGridBlockConfigurationAreaForm.alias.$setValidity("alias", $scope.model.otherAreaAliases.indexOf(newVal) === -1);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
index 84a64a7669..3c005663e8 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
@@ -241,7 +241,6 @@
infiniteMode: true,
noTemplate: true,
isElement: true,
- noTemplate: true,
submit: function (model) {
loadElementTypes().then( function () {
callback(model.documentTypeKey);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.controller.js
index 0bb9f6e703..11a0972309 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.controller.js
@@ -93,7 +93,7 @@
var elementTypeId = elementType.id;
const editor = {
id: elementTypeId,
- submit: function (model) {
+ submit: function () {
editorService.close();
},
close: function () {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.html
index 64a7909b7a..7ee2ee6344 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.html
@@ -2,33 +2,40 @@
-
modelEntry.key === el.dataset.areaKey,
+ querySelectModelToElement: (container, modelEntry) => container.querySelector(`[data-area-key='${modelEntry.key}']`),
+ itemHasNestedContainersResolver: () => false,// We never have nested in this case.
+ containerSelector: ".umb-block-grid-area-editor__grid-wrapper",
+ itemSelector: ".umb-block-grid-area-editor__area",
+ placeholderClass: "umb-block-grid-area-editor__area-placeholder",
+ onSync: onSortSync
}
- const gridContainerEl = $element[0].querySelector('.umb-block-grid-area-editor__grid-wrapper');
+ function onSortSync() {
+ $scope.$evalAsync();
+ setDirty();
+ }
- const sortable = Sortable.create(gridContainerEl, {
- sort: true, // sorting inside list
- animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
- easing: "cubic-bezier(1, 0, 0, 1)", // Easing for animation. Defaults to null. See https://easings.net/ for examples.
- cancel: '',
- draggable: ".umb-block-grid-area-editor__area", // Specifies which items inside the element should be draggable
- ghostClass: "umb-block-grid-area-editor__area-placeholder",
- onAdd: function (evt) {
- _sync(evt);
- $scope.$evalAsync();
- },
- onUpdate: function (evt) {
- _sync(evt);
- $scope.$evalAsync();
+ function resolveVerticalDirection(data) {
+
+ /** We need some data about the grid to figure out if there is room to be placed next to the found element */
+ const approvedContainerComputedStyles = getComputedStyle(data.containerElement);
+ const gridColumnGap = Number(approvedContainerComputedStyles.columnGap.split("px")[0]) || 0;
+ const gridColumnNumber = vm.rootLayoutColumns;
+
+ const foundElColumns = parseInt(data.relatedElement.dataset.colSpan, 10);
+ const currentElementColumns = data.item.columnSpan;
+
+ if(currentElementColumns >= gridColumnNumber) {
+ return true;
}
- });
-
- // TODO: setDirty if sort has happend.
+
+ // Get grid template:
+ const approvedContainerGridColumns = approvedContainerComputedStyles.gridTemplateColumns.trim().split("px").map(x => Number(x)).filter(n => n > 0).map((n, i, list) => list.length === i ? n : n + gridColumnGap);
+
+ // ensure all columns are there.
+ // This will also ensure handling non-css-grid mode,
+ // use container width divided by amount of columns( or the item width divided by its amount of columnSpan)
+ let amountOfColumnsInWeightMap = approvedContainerGridColumns.length;
+ const amountOfUnknownColumns = gridColumnNumber-amountOfColumnsInWeightMap;
+ if(amountOfUnknownColumns > 0) {
+ let accumulatedValue = getAccumulatedValueOfIndex(amountOfColumnsInWeightMap, approvedContainerGridColumns) || 0;
+ const layoutWidth = data.containerRect.width;
+ const missingColumnWidth = (layoutWidth-accumulatedValue)/amountOfUnknownColumns;
+ if(missingColumnWidth > 0) {
+ while(amountOfColumnsInWeightMap++ < gridColumnNumber) {
+ approvedContainerGridColumns.push(missingColumnWidth);
+ }
+ }
+ }
+
+ let offsetPlacement = 0;
+ /* If placeholder is in this same line, we want to assume that it will offset the placement of the found element,
+ which provides more potential space for the item to drop at.
+ This is relevant in this calculation where we look at the space to determine if its a vertical or horizontal drop in relation to the found element.
+ */
+ if(data.placeholderIsInThisRow && data.elementRect.left < data.relatedRect.left) {
+ offsetPlacement = -(data.elementRect.width + gridColumnGap);
+ }
+
+ const relatedStartX = Math.max(data.relatedRect.left - data.containerRect.left + offsetPlacement, 0);
+ const relatedStartCol = Math.round(getInterpolatedIndexOfPositionInWeightMap(relatedStartX, approvedContainerGridColumns));
+
+ // If the found related element does not have enough room after which for the current element, then we go vertical mode:
+ return (relatedStartCol + (data.horizontalPlaceAfter ? foundElColumns : 0) + currentElementColumns > gridColumnNumber);
+
+ }
+
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umbBlockGridColumnEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umbBlockGridColumnEditor.component.js
index 864f479009..1dcbc5b2da 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umbBlockGridColumnEditor.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umbBlockGridColumnEditor.component.js
@@ -25,9 +25,7 @@
}
});
- function BlockGridColumnController($scope) {
-
- //var unsubscribe = [];
+ function BlockGridColumnController() {
var vm = this;
@@ -58,12 +56,6 @@
}
}
- /*$scope.$on("$destroy", function () {
- for (const subscription of unsubscribe) {
- subscription();
- }
- });*/
-
}
})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entries.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entries.html
index 4aed670ffd..ef02d28318 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entries.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entries.html
@@ -2,9 +2,10 @@
rect.left - modifier && x < rect.right + modifier && y > rect.top - modifier && y < rect.bottom + modifier);
+ function getAccumulatedValueOfIndex(index, weights) {
+ let i = 0, len = Math.min(index, weights.length), calc = 0;
+ while(i area.key === vm.areaKey);
@@ -90,10 +94,6 @@
unsubscribe.push($scope.$watch('vm.entries', onLocalAmountOfBlocksChanged, true));
};
- unsubscribe.push($scope.$watch("layoutColumns", (newVal, oldVal) => {
- vm.layoutColumnsInt = parseInt(vm.layoutColumns, 10);
- }));
-
function onLocalAmountOfBlocksChanged() {
if (vm.entriesForm && vm.areaConfig) {
@@ -153,440 +153,159 @@
}
}
-
- vm.notifyVisualUpdate = function () {
+ // Used by umb block grid entries component, to trigger other blocks to update.
+ vm.notifyVisualUpdate = notifyVisualUpdate;
+ function notifyVisualUpdate() {
$scope.$broadcast("blockGridEditorVisualUpdate", {areaKey: vm.areaKey});
}
- vm.acceptBlock = function(contentTypeKey) {
- return vm.blockEditorApi.internal.isElementTypeKeyAllowedAt(vm.parentBlock, vm.areaKey, contentTypeKey);
+ function removeAllContainedPropertyEditorProxies() {
+ currentContainedPropertyEditorProxies.forEach(slotName => {
+ removePropertyEditorProxies(slotName);
+ });
+ }
+ function removePropertyEditorProxies(slotName) {
+ const event = new CustomEvent("UmbBlockGrid_RemoveProperty", {composed: true, bubbles: true, detail: {'slotName': slotName}});
+ $element[0].dispatchEvent(event);
}
- vm.getLayoutEntryByIndex = function(index) {
- return vm.blockEditorApi.internal.getLayoutEntryByIndex(vm.parentBlock, vm.areaKey, index);
+
+
+
+ function resolveVerticalDirection(data) {
+
+ /** We need some data about the grid to figure out if there is room to be placed next to the found element */
+ const approvedContainerComputedStyles = getComputedStyle(data.containerElement);
+ const gridColumnGap = Number(approvedContainerComputedStyles.columnGap.split("px")[0]) || 0;
+ const gridColumnNumber = parseInt(approvedContainerComputedStyles.getPropertyValue("--umb-block-grid--grid-columns"), 10);
+
+
+ const foundElColumns = parseInt(data.relatedElement.dataset.colSpan, 10);
+ const currentElementColumns = data.item.columnSpan;
+
+ if(currentElementColumns >= gridColumnNumber) {
+ return true;
+ }
+
+ // Get grid template:
+ const approvedContainerGridColumns = approvedContainerComputedStyles.gridTemplateColumns.trim().split("px").map(x => Number(x)).filter(n => n > 0).map((n, i, list) => list.length === i ? n : n + gridColumnGap);
+
+ // ensure all columns are there.
+ // This will also ensure handling non-css-grid mode,
+ // use container width divided by amount of columns( or the item width divided by its amount of columnSpan)
+ let amountOfColumnsInWeightMap = approvedContainerGridColumns.length;
+ const amountOfUnknownColumns = gridColumnNumber-amountOfColumnsInWeightMap;
+ if(amountOfUnknownColumns > 0) {
+ let accumulatedValue = getAccumulatedValueOfIndex(amountOfColumnsInWeightMap, approvedContainerGridColumns) || 0;
+ const layoutWidth = data.containerRect.width;
+ const missingColumnWidth = (layoutWidth-accumulatedValue)/amountOfUnknownColumns;
+ if(missingColumnWidth > 0) {
+ while(amountOfColumnsInWeightMap++ < gridColumnNumber) {
+ approvedContainerGridColumns.push(missingColumnWidth);
+ }
+ }
+ }
+
+ let offsetPlacement = 0;
+ /* If placeholder is in this same line, we want to assume that it will offset the placement of the found element,
+ which provides more potential space for the item to drop at.
+ This is relevant in this calculation where we look at the space to determine if its a vertical or horizontal drop in relation to the found element.
+ */
+ if(data.placeholderIsInThisRow && data.elementRect.left < data.relatedRect.left) {
+ offsetPlacement = -(data.elementRect.width + gridColumnGap);
+ }
+
+ const relatedStartX = Math.max(data.relatedRect.left - data.containerRect.left + offsetPlacement, 0);
+ const relatedStartCol = Math.round(getInterpolatedIndexOfPositionInWeightMap(relatedStartX, approvedContainerGridColumns));
+
+ // If the found related element does not have enough room after which for the current element, then we go vertical mode:
+ return (relatedStartCol + (data.horizontalPlaceAfter ? foundElColumns : 0) + currentElementColumns > gridColumnNumber);
}
- vm.showNotAllowed = function() {
+
+
+
+ function initializeSorter() {
+ vm.sorterOptions = {
+ resolveVerticalDirection: resolveVerticalDirection,
+ dataTransferResolver: (dataTransfer, item) => {dataTransfer.setData("text/plain", item.$block.label)}, // (Optional) Append OS data to the moved item.
+ compareElementToModel: (el, modelEntry) => modelEntry.contentUdi === el.dataset.elementUdi,
+ querySelectModelToElement: (container, modelEntry) => container.querySelector(`[data-element-udi='${modelEntry.contentUdi}']`),
+ itemHasNestedContainersResolver: (foundEl) => foundEl.classList.contains('--has-areas'), // (Optional) improve performance for recognizing if an items has inner containers.
+ identifier: "BlockGridEditor_"+vm.blockEditorApi.internal.uniqueEditorKey,
+ boundarySelector: ".umb-block-grid__area", // (Optional) Used for extended boundary between containers.
+ containerSelector: ".umb-block-grid__layout-container", // Used for connecting with others
+ itemSelector: ".umb-block-grid__layout-item",
+ draggableSelector: ".umb-block-grid__block--view",
+ placeholderClass: "umb-block-grid__layout-item-placeholder",
+ ghostClass: "umb-block-grid__layout-item-ghost",
+ onStart: onSortStart,
+ onEnd: onSortEnd,
+ onSync: onSortSync,
+ onDisallowed: onSortDisallowed,
+ onAllowed: onSortAllowed,
+ onRequestDrop: onSortRequestDrop
+ }
+ }
+
+ function onSortStart(data) {
+ // Gather containedPropertyEditorProxies from this element.
+ currentContainedPropertyEditorProxies = Array.from(data.element.querySelectorAll('slot[data-is-property-editor-proxy]')).map(x => x.getAttribute('name'));
+ vm.blockEditorApi.internal.startDraggingMode();
+ }
+
+ function onSortEnd() {
+ vm.blockEditorApi.internal.exitDraggingMode();
+ currentContainedPropertyEditorProxies = [];
+ notifyVisualUpdate();
+ $scope.$evalAsync();
+ }
+
+ function onSortSync(data) {
+ if (data.fromController !== data.toController) {
+ removeAllContainedPropertyEditorProxies();
+
+ const contextColumns = vm.blockEditorApi.internal.getContextColumns(vm.parentBlock, vm.areaKey);
+
+ // if colSpan is lower than contextColumns, and we do have some columnSpanOptions:
+ if (data.item.columnSpan < contextColumns && data.item.$block.config.columnSpanOptions.length > 0) {
+ // then check if the colSpan is a columnSpanOption, if NOT then reset to contextColumns.
+ const found = data.item.$block.config.columnSpanOptions.find(option => option.columnSpan === data.item.columnSpan);
+ if(!found) {
+ data.item.columnSpan = contextColumns;
+ }
+ } else {
+ data.item.columnSpan = contextColumns;
+ }
+ }
+ $scope.$evalAsync();
+ vm.blockEditorApi.internal.setDirty();
+ }
+
+ function onSortDisallowed() {
vm.showNotAllowedUI = true;
$scope.$evalAsync();
}
- vm.hideNotAllowed = function() {
+ function onSortAllowed() {
vm.showNotAllowedUI = false;
$scope.$evalAsync();
}
-
- var revertIndicateDroppableTimeout;
- vm.revertIndicateDroppable = function() {
- revertIndicateDroppableTimeout = $timeout(() => {
- vm.droppableIndication = false;
- }, 2000);
- }
- vm.indicateDroppable = function() {
- if (revertIndicateDroppableTimeout) {
- $timeout.cancel(revertIndicateDroppableTimeout);
- revertIndicateDroppableTimeout = null;
- }
- vm.droppableIndication = true;
- $scope.$evalAsync();
+ function onSortRequestDrop(data) {
+ return vm.blockEditorApi.internal.isElementTypeKeyAllowedAt(vm.parentBlock, vm.areaKey, data.item.$block.config.contentElementTypeKey);
}
- function initializeSortable() {
- const gridLayoutContainerEl = $element[0].querySelector('.umb-block-grid__layout-container');
- var _lastIndicationContainerVM = null;
- var targetRect = null;
- var relatedEl = null;
- var ghostEl = null;
- var ghostRect = null;
- var dragX = 0;
- var dragY = 0;
- var dragOffsetX = 0;
- var approvedContainerEl = null;
- // Setup DOM method for communication between sortables:
- gridLayoutContainerEl['Sortable:controller'] = () => {
- return vm;
- };
- var nextSibling;
- function _removePropertyProxy(eventTarget, slotName) {
- const event = new CustomEvent("UmbBlockGrid_RemoveProperty", {composed: true, bubbles: true, detail: {'slotName': slotName}});
- eventTarget.dispatchEvent(event);
+
+ $scope.$on('$destroy', function () {
+ for (const subscription of unsubscribe) {
+ subscription();
}
-
- // Borrowed concept from, its not identical as more has been implemented: https://github.com/SortableJS/angular-legacy-sortablejs/blob/master/angular-legacy-sortable.js
- function _sync(evt) {
-
- const oldIndex = evt.oldIndex,
- newIndex = evt.newIndex;
-
- // If not the same gridLayoutContainerEl, then test for transfer option:
- if (gridLayoutContainerEl !== evt.from) {
- const fromCtrl = evt.from['Sortable:controller']();
- const prevEntries = fromCtrl.entries;
- const syncEntry = prevEntries[oldIndex];
-
- // Make sure Property Editor Proxies are destroyed, as we need to establish new when moving context:
-
-
- // unregister all property editor proxies via events:
- fromCtrl.containedPropertyEditorProxies.forEach(slotName => {
- _removePropertyProxy(evt.from, slotName);
- });
-
- // Perform the transfer:
- if (Sortable.active && Sortable.active.lastPullMode === 'clone') {
- syncEntry = Utilities.copy(syncEntry);
- prevEntries.splice(Sortable.utils.index(evt.clone, sortable.options.draggable), 0, prevEntries.splice(oldIndex, 1)[0]);
- }
- else {
- prevEntries.splice(oldIndex, 1);
- }
- vm.entries.splice(newIndex, 0, syncEntry);
-
- const contextColumns = vm.blockEditorApi.internal.getContextColumns(vm.parentBlock, vm.areaKey);
-
- // if colSpan is lower than contextColumns, and we do have some columnSpanOptions:
- if (syncEntry.columnSpan < contextColumns && syncEntry.$block.config.columnSpanOptions.length > 0) {
- // then check if the colSpan is a columnSpanOption, if NOT then reset to contextColumns.
- const found = syncEntry.$block.config.columnSpanOptions.find(option => option.columnSpan === syncEntry.columnSpan);
- if(!found) {
- syncEntry.columnSpan = contextColumns;
- }
- } else {
- syncEntry.columnSpan = contextColumns;
- }
-
- }
- else {
- vm.entries.splice(newIndex, 0, vm.entries.splice(oldIndex, 1)[0]);
- }
- }
-
- function _indication(contextVM, movingEl) {
-
- // Remove old indication:
- if(_lastIndicationContainerVM !== contextVM && _lastIndicationContainerVM !== null) {
- _lastIndicationContainerVM.hideNotAllowed();
- _lastIndicationContainerVM.revertIndicateDroppable();
- }
- _lastIndicationContainerVM = contextVM;
-
- if(contextVM.acceptBlock(movingEl.dataset.contentElementTypeKey) === true) {
- _lastIndicationContainerVM.hideNotAllowed();
- _lastIndicationContainerVM.indicateDroppable();// This block is accepted so we will indicate a good drop.
- return true;
- }
-
- contextVM.showNotAllowed();// This block is not accepted to we will indicate that its not allowed.
-
- return false;
- }
-
- function _moveGhostElement() {
-
- rqaId = null;
- if(!ghostEl) {
- return;
- }
- if(!approvedContainerEl) {
- console.error("Cancel cause had no approvedContainerEl", approvedContainerEl)
- return;
- }
-
- ghostRect = ghostEl.getBoundingClientRect();
-
- const insideGhost = isWithinRect(dragX, dragY, ghostRect);
- if (insideGhost) {
- return;
- }
-
- var approvedContainerRect = approvedContainerEl.getBoundingClientRect();
-
- const approvedContainerHasItems = approvedContainerEl.querySelector('.umb-block-grid__layout-item:not(.umb-block-grid__layout-item-placeholder)');
- if(!approvedContainerHasItems && isWithinRect(dragX, dragY, approvedContainerRect, 20) || approvedContainerHasItems && isWithinRect(dragX, dragY, approvedContainerRect, -10)) {
- // we are good...
- } else {
- var parentContainer = approvedContainerEl.parentNode.closest('.umb-block-grid__layout-container');
- if(parentContainer) {
-
- if(parentContainer['Sortable:controller']().sortGroupIdentifier === vm.sortGroupIdentifier) {
- approvedContainerEl = parentContainer;
- approvedContainerRect = approvedContainerEl.getBoundingClientRect();
- }
- }
- }
-
- // gather elements on the same row.
- let elementInSameRow = [];
- const containerElements = Array.from(approvedContainerEl.children);
- for (const el of containerElements) {
- const elRect = el.getBoundingClientRect();
- // gather elements on the same row.
- if(dragY >= elRect.top && dragY <= elRect.bottom && el !== ghostEl) {
- elementInSameRow.push({el: el, rect:elRect});
- }
- }
-
- let lastDistance = 99999;
- let foundRelatedEl = null;
- let placeAfter = false;
- elementInSameRow.forEach( sameRow => {
- const centerX = (sameRow.rect.left + (sameRow.rect.width*.5));
- let distance = Math.abs(dragX - centerX);
- if(distance < lastDistance) {
- foundRelatedEl = sameRow.el;
- lastDistance = Math.abs(distance);
- placeAfter = dragX > centerX;
- }
- });
-
- if (foundRelatedEl === ghostEl) {
- return;
- }
-
- if (foundRelatedEl) {
-
-
- let newIndex = containerElements.indexOf(foundRelatedEl);
-
- const foundRelatedElRect = foundRelatedEl.getBoundingClientRect();
-
- // Ghost is already on same line and we are not hovering the related element?
- const ghostCenterY = ghostRect.top + (ghostRect.height*.5);
- const isInsideFoundRelated = isWithinRect(dragX, dragY, foundRelatedElRect, 0);
-
-
- if (isInsideFoundRelated && foundRelatedEl.classList.contains('--has-areas')) {
- // If mouse is on top of an area, then make that the new approvedContainer?
- const blockView = foundRelatedEl.querySelector('.umb-block-grid__block--view');
- const subLayouts = blockView.querySelectorAll('.umb-block-grid__layout-container');
- for (const subLayout of subLayouts) {
- const subLayoutRect = subLayout.getBoundingClientRect();
- const hasItems = subLayout.querySelector('.umb-block-grid__layout-item:not(.umb-block-grid__layout-item-placeholder)');
- // gather elements on the same row.
- if(!hasItems && isWithinRect(dragX, dragY, subLayoutRect, 20) || hasItems && isWithinRect(dragX, dragY, subLayoutRect, -10)) {
-
- var subVm = subLayout['Sortable:controller']();
- if(subVm.sortGroupIdentifier === vm.sortGroupIdentifier) {
- approvedContainerEl = subLayout;
- _moveGhostElement();
- return;
- }
- }
- }
- }
-
- if (ghostCenterY > foundRelatedElRect.top && ghostCenterY < foundRelatedElRect.bottom && !isInsideFoundRelated) {
- return;
- }
-
- const containerVM = approvedContainerEl['Sortable:controller']();
- if(_indication(containerVM, ghostEl) === false) {
- return;
- }
-
- let verticalDirection = false;
-
- // TODO: move calculations out so they can be persisted a bit longer?
- //const approvedContainerRect = approvedContainerEl.getBoundingClientRect();
- const approvedContainerComputedStyles = getComputedStyle(approvedContainerEl);
- const gridColumnNumber = parseInt(approvedContainerComputedStyles.getPropertyValue("--umb-block-grid--grid-columns"), 10);
-
- const relatedColumns = parseInt(foundRelatedEl.dataset.colSpan, 10);
- const ghostColumns = parseInt(ghostEl.dataset.colSpan, 10);
-
- // Get grid template:
- const approvedContainerGridColumns = approvedContainerComputedStyles.gridTemplateColumns.trim().split("px").map(x => Number(x)).filter(n => n > 0);
-
- // ensure all columns are there.
- // This will also ensure handling non-css-grid mode,
- // use container width divided by amount of columns( or the item width divided by its amount of columnSpan)
- let amountOfColumnsInWeightMap = approvedContainerGridColumns.length;
- const amountOfUnknownColumns = gridColumnNumber-amountOfColumnsInWeightMap;
- if(amountOfUnknownColumns > 0) {
- let accumulatedValue = getAccumulatedValueOfIndex(amountOfColumnsInWeightMap, approvedContainerGridColumns) || 0;
- const layoutWidth = approvedContainerRect.width;
- const missingColumnWidth = (layoutWidth-accumulatedValue)/amountOfUnknownColumns;
- while(amountOfColumnsInWeightMap++ < gridColumnNumber) {
- approvedContainerGridColumns.push(missingColumnWidth);
- }
- }
-
-
- const relatedStartX = foundRelatedElRect.left - approvedContainerRect.left;
- const relatedStartCol = Math.round(getInterpolatedIndexOfPositionInWeightMap(relatedStartX, approvedContainerGridColumns));
-
- if(relatedStartCol + relatedColumns + ghostColumns > gridColumnNumber) {
- verticalDirection = true;
- }
-
- if (verticalDirection) {
- placeAfter = (dragY > foundRelatedElRect.top + (foundRelatedElRect.height*.5));
- }
-
-
- const nextEl = containerElements[(placeAfter ? newIndex+1 : newIndex)];
- if (nextEl) {
- approvedContainerEl.insertBefore(ghostEl, nextEl);
- } else {
- approvedContainerEl.appendChild(ghostEl);
- }
-
- return;
- }
-
- // If above or below container, we will go first or last.
- const containerVM = approvedContainerEl['Sortable:controller']();
- if(_indication(containerVM, ghostEl) === false) {
- return;
- }
- if(dragY < approvedContainerRect.top) {
- const firstEl = containerElements[0];
- if (firstEl) {
- approvedContainerEl.insertBefore(ghostEl, firstEl);
- } else {
- approvedContainerEl.appendChild(ghostEl);
- }
- } else if(dragY > approvedContainerRect.bottom) {
- approvedContainerEl.appendChild(ghostEl);
- }
- }
-
- var rqaId = null
- function _onDragMove(evt) {
-
- const clientX = (evt.touches ? evt.touches[0] : evt).clientX;
- const clientY = (evt.touches ? evt.touches[1] : evt).clientY;
- if(vm.movingLayoutEntry && targetRect && ghostRect && clientX !== 0 && clientY !== 0) {
-
- if(dragX === clientX && dragY === clientY) {
- return;
- }
- dragX = clientX;
- dragY = clientY;
-
- ghostRect = ghostEl.getBoundingClientRect();
-
- const insideGhost = isWithinRect(dragX, dragY, ghostRect, 0);
-
- if (!insideGhost) {
- if(rqaId === null) {
- rqaId = requestAnimationFrame(_moveGhostElement);
- }
- }
- }
- }
-
- vm.sortGroupIdentifier = "BlockGridEditor_"+vm.blockEditorApi.internal.uniqueEditorKey;
-
- const sortable = Sortable.create(gridLayoutContainerEl, {
- group: vm.sortGroupIdentifier,
- sort: true,
- animation: 0,
- cancel: '',
- draggable: ".umb-block-grid__layout-item",
- ghostClass: "umb-block-grid__layout-item-placeholder",
- swapThreshold: .4,
- dragoverBubble: true,
- emptyInsertThreshold: 40,
-
- scrollSensitivity: 50,
- scrollSpeed: 16,
- scroll: true,
- forceAutoScrollFallback: true,
-
- onStart: function (evt) {
-
- // TODO: This does not work correctly jet with SortableJS. With the replacement we should be able to call this before DOM is changed.
- vm.blockEditorApi.internal.startDraggingMode();
-
- nextSibling = evt.from === evt.item.parentNode ? evt.item.nextSibling : evt.clone.nextSibling;
-
- var contextVM = vm;
- if (gridLayoutContainerEl !== evt.to) {
- contextVM = evt.to['Sortable:controller']();
- }
-
- approvedContainerEl = evt.to;
-
- const oldIndex = evt.oldIndex;
- vm.movingLayoutEntry = contextVM.getLayoutEntryByIndex(oldIndex);
-
- ghostEl = evt.item;
- vm.containedPropertyEditorProxies = Array.from(ghostEl.querySelectorAll('slot[data-is-property-editor-proxy]')).map(x => x.getAttribute('name'));
-
- targetRect = evt.to.getBoundingClientRect();
- ghostRect = ghostEl.getBoundingClientRect();
-
- const clientX = (evt.originalEvent.touches ? evt.originalEvent.touches[0] : evt.originalEvent).clientX;
- dragOffsetX = clientX - ghostRect.left;
-
- window.addEventListener('drag', _onDragMove);
- window.addEventListener('dragover', _onDragMove);
-
- $scope.$evalAsync();
- },
- // Called by any change to the list (add / update / remove)
- onMove: function (evt) {
- relatedEl = evt.related;
- targetRect = evt.to.getBoundingClientRect();
- ghostRect = evt.draggedRect;
-
- // Disable SortableJS from handling the drop, instead we will use our own.
- return false;
- },
- // When an change actually was made, after drop has occurred:
- onSort: function (evt) {
- vm.blockEditorApi.internal.setDirty();
- },
-
- onAdd: function (evt) {
- _sync(evt);
- $scope.$evalAsync();
- },
- onUpdate: function (evt) {
- _sync(evt);
- $scope.$evalAsync();
- },
- onEnd: function(evt) {
- if(rqaId !== null) {
- cancelAnimationFrame(rqaId);
- }
- window.removeEventListener('drag', _onDragMove);
- window.removeEventListener('dragover', _onDragMove);
- vm.blockEditorApi.internal.exitDraggingMode();
-
- // ensure not-allowed indication is removed.
- if(_lastIndicationContainerVM) {
- _lastIndicationContainerVM.hideNotAllowed();
- _lastIndicationContainerVM.revertIndicateDroppable();
- _lastIndicationContainerVM = null;
- }
-
- approvedContainerEl = null;
- vm.movingLayoutEntry = null;
- targetRect = null;
- ghostRect = null;
- ghostEl = null;
- relatedEl = null;
- vm.containedPropertyEditorProxies = [];
-
- vm.notifyVisualUpdate();
- }
- });
-
- $scope.$on('$destroy', function () {
- sortable.destroy();
- for (const subscription of unsubscribe) {
- subscription();
- }
- });
-
- };
+ });
}
})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js
index abc1ec5016..28c4aa6847 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js
@@ -159,7 +159,7 @@
$scope.$evalAsync();
}
- unsubscribe.push($scope.$watch("depth", (newVal, oldVal) => {
+ unsubscribe.push($scope.$watch("depth", () => {
vm.childDepth = parseInt(vm.depth) + 1;
}));
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridsortable.directive.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridsortable.directive.js
new file mode 100644
index 0000000000..fc973ce490
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridsortable.directive.js
@@ -0,0 +1,706 @@
+(function () {
+ 'use strict';
+
+ function isWithinRect(x, y, rect, modifier) {
+ return (x > rect.left - modifier && x < rect.right + modifier && y > rect.top - modifier && y < rect.bottom + modifier);
+ }
+
+
+ function getParentScrollElement(el, includeSelf) {
+ // skip to window
+ if (!el || !el.getBoundingClientRect) return null;
+ var elem = el;
+ var gotSelf = false;
+
+ while(elem) {
+
+ // we don't need to get elem css if it isn't even overflowing in the first place (performance)
+ if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) {
+ var elemCSS = getComputedStyle(elem);
+
+ if (
+ elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')
+ ||
+ elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll')
+ ) {
+ if (!elem.getBoundingClientRect || elem === document.body) return null;
+ if (gotSelf || includeSelf) return elem;
+ gotSelf = true;
+ }
+ }
+
+ if(elem.parentNode === document) {
+ return null;
+ } else if(elem.parentNode instanceof DocumentFragment) {
+ elem = elem.parentNode.host;
+ } else {
+ elem = elem.parentNode;
+ }
+ }
+
+ return null;
+ }
+
+
+
+
+ const DefaultConfig = {
+ compareElementToModel: (el, modelEntry) => modelEntry.contentUdi === el.dataset.elementUdi,
+ querySelectModelToElement: (container, modelEntry) => container.querySelector(`[data-element-udi='${modelEntry.contentUdi}']`),
+ identifier: "UmbBlockGridSorter",
+ containerSelector: "ol", // To find container and to connect with others.
+ ignorerSelector: "a, img, iframe",
+ itemSelector: "li",
+ placeholderClass: "umb-drag-placeholder"
+ }
+
+
+
+ function UmbBlockGridSorter() {
+
+ function link(scope, element) {
+
+
+ let observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ mutation.addedNodes.forEach(function(addedNode) {
+ if (addedNode.matches && addedNode.matches(scope.config.itemSelector)) {
+ setupItem(addedNode);
+ }
+ });
+ mutation.removedNodes.forEach(function(removedNode) {
+ if (removedNode.matches && removedNode.matches(scope.config.itemSelector)) {
+ destroyItem(removedNode);
+ }
+ });
+ });
+ });
+
+
+ let vm = {};
+
+ const config = {...DefaultConfig, ...scope.config};
+
+ vm.identifier = config.identifier;
+
+ let scrollElement = null;
+
+ let containerEl = config.containerSelector ? element[0].closest(config.containerSelector) : element[0];
+ if (!containerEl) {
+ console.error("Could not initialize umb block grid sorter.", element[0])
+ return;
+ }
+
+
+
+ function init() {
+
+ containerEl['umbBlockGridSorter:vm'] = () => {
+ return vm;
+ };
+ containerEl.addEventListener('dragover', preventDragOver);
+
+ observer.observe(containerEl, {childList: true, subtree: false});
+ }
+ init();
+
+ function preventDragOver(e) {
+ e.preventDefault()
+ }
+
+ function setupItem(element) {
+
+ setupIgnorerElements(element);
+
+ element.draggable = true;
+ element.addEventListener('dragstart', handleDragStart);
+ }
+
+ function destroyItem(element) {
+
+ destroyIgnorerElements(element);
+
+ element.removeEventListener('dragstart', handleDragStart);
+ }
+
+ function setupIgnorerElements(element) {
+ config.ignorerSelector.split(',').forEach(function (criteria) {
+ element.querySelectorAll(criteria.trim()).forEach(setupPreventEvent);
+ });
+ }
+ function destroyIgnorerElements(element) {
+ config.ignorerSelector.split(',').forEach(function (criteria) {
+ element.querySelectorAll(criteria.trim()).forEach(destroyPreventEvent);
+ });
+ }
+ function setupPreventEvent(element) {
+ element.draggable = false
+ }
+ function destroyPreventEvent(element) {
+ element.removeAttribute('draggable');
+ }
+
+
+
+
+
+ let currentContainerElement = containerEl;
+ let currentContainerVM = vm;
+
+ let rqaId = null;
+ let currentItem = null;
+ let currentElement = null;
+ let currentDragElement = null;
+ let currentDragRect = null;
+ let dragX = 0;
+ let dragY = 0;
+
+ function handleDragStart(event) {
+
+ if(currentElement) {
+ handleDragEnd();
+ }
+
+ event.stopPropagation();
+ event.dataTransfer.effectAllowed = "move";// copyMove when we enhance the drag with clipboard data.
+ event.dataTransfer.dropEffect = "none";// visual feedback when dropped.
+
+ if(!scrollElement) {
+ scrollElement = getParentScrollElement(containerEl, true);
+ }
+
+ const element = event.target.closest(config.itemSelector);
+
+ currentElement = element;
+ currentDragElement = config.draggableSelector ? currentElement.querySelector(config.draggableSelector) : currentElement;
+ currentDragRect = currentDragElement.getBoundingClientRect();
+ currentItem = vm.getItemOfElement(currentElement);
+ if(!currentItem) {
+ console.error("Could not find item related to this element.");
+ return;
+ }
+
+ currentElement.style.transform = 'translateZ(0)';// Solves problem with FireFox and ShadowDom in the drag-image.
+
+ if (config.dataTransferResolver) {
+ config.dataTransferResolver(event.dataTransfer, currentItem);
+ }
+
+ if (config.onStart) {
+ config.onStart({item: currentItem, element: currentElement});
+ }
+
+ window.addEventListener('dragover', handleDragMove);
+ window.addEventListener('dragend', handleDragEnd);
+
+ // We must wait one frame before changing the look of the block.
+ rqaId = requestAnimationFrame(() => {// It should be okay to use the same refId, as the move does not or is okay not to happen on first frame/drag-move.
+ rqaId = null;
+ currentElement.style.transform = '';
+ currentElement.classList.add(config.placeholderClass);
+ });
+
+ }
+
+ function handleDragEnd() {
+
+ if(!currentElement) {
+ return;
+ }
+
+ window.removeEventListener('dragover', handleDragMove);
+ window.removeEventListener('dragend', handleDragEnd);
+ currentElement.style.transform = '';
+ currentElement.classList.remove(config.placeholderClass);
+
+ stopAutoScroll();
+ removeAllowIndication();
+
+ if(currentContainerVM.sync(currentElement, vm) === false) {
+ // Sync could not succeed, might be because item is not allowed here.
+
+ // Lets move the Element back to where it came from:
+ const movingItemIndex = scope.model.indexOf(currentItem);
+ if(movingItemIndex < scope.model.length-1) {
+ const afterItem = scope.model[movingItemIndex+1];
+ const afterEl = config.querySelectModelToElement(containerEl, afterItem);
+ containerEl.insertBefore(currentElement, afterEl);
+ } else {
+ containerEl.appendChild(currentElement);
+ }
+
+ currentContainerVM = vm;
+ }
+
+ if (config.onEnd) {
+ config.onEnd({item: currentItem, element: currentElement});
+ }
+
+ if(rqaId) {
+ cancelAnimationFrame(rqaId);
+ }
+
+ currentContainerElement = containerEl;
+ currentContainerVM = vm;
+
+ rqaId = null
+ currentItem = null;
+ currentElement = null;
+ currentDragElement = null;
+ currentDragRect = null;
+ dragX = 0;
+ dragY = 0;
+
+ }
+
+ function handleDragMove(event) {
+
+ if(!currentElement) {
+ return;
+ }
+
+ const clientX = (event.touches ? event.touches[0] : event).clientX;
+ const clientY = (event.touches ? event.touches[1] : event).clientY;
+ if(clientX !== 0 && clientY !== 0) {
+
+
+ if(dragX === clientX && dragY === clientY) {
+ return;
+ }
+ dragX = clientX;
+ dragY = clientY;
+
+ handleAutoScroll(dragX, dragY);
+
+ currentDragRect = currentDragElement.getBoundingClientRect();
+ const insideCurrentRect = isWithinRect(dragX, dragY, currentDragRect, 0);
+ if (!insideCurrentRect) {
+ if(rqaId === null) {
+ rqaId = requestAnimationFrame(moveCurrentElement);
+ }
+ }
+ }
+ }
+
+
+
+ function moveCurrentElement() {
+
+ rqaId = null;
+ if(!currentElement) {
+ return;
+ }
+
+ const currentElementRect = currentElement.getBoundingClientRect();
+ const insideCurrentRect = isWithinRect(dragX, dragY, currentElementRect);
+ if (insideCurrentRect) {
+ return;
+ }
+
+ // If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement.
+ var currentBoundaryElement = (config.boundarySelector ? currentContainerElement.closest(config.boundarySelector) : currentContainerElement) || currentContainerElement;
+
+ var currentBoundaryRect = currentBoundaryElement.getBoundingClientRect();
+
+ const currentContainerHasItems = currentContainerVM.hasOtherItemsThan(currentItem);
+
+ // if empty we will be move likely to accept an item (add 20px to the bounding box)
+ // If we have items we must be 10 within the container to accept the move.
+ const offsetEdge = currentContainerHasItems ? -10 : 20;
+ if(!isWithinRect(dragX, dragY, currentBoundaryRect, offsetEdge)) {
+ // we are outside the current container boundary, so lets see if there is a parent we can move.
+ var parentContainer = currentContainerElement.parentNode.closest(config.containerSelector);
+ if(parentContainer) {
+ const parentContainerVM = parentContainer['umbBlockGridSorter:vm']();
+ if(parentContainerVM.identifier === vm.identifier) {
+ currentContainerElement = parentContainer;
+ currentContainerVM = parentContainerVM;
+ }
+ }
+ }
+
+
+
+
+ // We want to retrieve the children of the container, every time to ensure we got the right order and index
+ const orderedContainerElements = Array.from(currentContainerElement.children);
+
+ var currentContainerRect = currentContainerElement.getBoundingClientRect();
+
+
+ // gather elements on the same row.
+ let elementsInSameRow = [];
+ let placeholderIsInThisRow = false;
+ for (const el of orderedContainerElements) {
+ const elRect = el.getBoundingClientRect();
+ // gather elements on the same row.
+ if(dragY >= elRect.top && dragY <= elRect.bottom) {
+
+ const dragElement = config.draggableSelector ? el.querySelector(config.draggableSelector) : el;
+ const dragElementRect = dragElement.getBoundingClientRect();
+ if(el !== currentElement) {
+ elementsInSameRow.push({el:el, dragRect:dragElementRect});
+ } else {
+ placeholderIsInThisRow = true;
+ }
+ }
+ }
+
+ let lastDistance = 99999;
+ let foundEl = null;
+ let foundElDragRect = null;
+ let placeAfter = false;
+ elementsInSameRow.forEach( sameRow => {
+ const centerX = (sameRow.dragRect.left + (sameRow.dragRect.width*.5));
+ let distance = Math.abs(dragX - centerX);
+ if(distance < lastDistance) {
+ foundEl = sameRow.el;
+ foundElDragRect = sameRow.dragRect;
+ lastDistance = Math.abs(distance);
+ placeAfter = dragX > centerX;
+ }
+ });
+
+ // If we are on top or closest to our self, we should not do anything.
+ if (foundEl === currentElement) {
+ return;
+ }
+
+ if (foundEl) {
+
+ const isInsideFound = isWithinRect(dragX, dragY, foundElDragRect, 0);
+
+ // If we are inside the found element, lets look for sub containers.
+ // use the itemHasNestedContainersResolver, if not configured fallback to looking for the existence of a container via DOM.
+ if (isInsideFound &&
+ config.itemHasNestedContainersResolver ? config.itemHasNestedContainersResolver(foundEl) : foundEl.querySelector(config.containerSelector)) {
+
+ // Find all sub containers:
+ const subLayouts = foundEl.querySelectorAll(config.containerSelector);
+ for (const subLayoutEl of subLayouts) {
+
+ // Use boundary element or fallback to container element.
+ var subBoundaryElement = (config.boundarySelector ? subLayoutEl.closest(config.boundarySelector) : subLayoutEl) || subLayoutEl;
+ var subBoundaryRect = subBoundaryElement.getBoundingClientRect();
+
+ const subContainerHasItems = subLayoutEl.querySelector(config.itemSelector+':not(.'+config.placeholderClass+')');
+ // gather elements on the same row.
+ const subOffsetEdge = subContainerHasItems ? -10 : 20;
+ if(isWithinRect(dragX, dragY, subBoundaryRect, subOffsetEdge)) {
+
+ var subVm = subLayoutEl['umbBlockGridSorter:vm']();
+ if(subVm.identifier === vm.identifier) {
+ currentContainerElement = subLayoutEl;
+ currentContainerVM = subVm;
+ moveCurrentElement();
+ return;
+ }
+ }
+ }
+ }
+
+
+ // Indication if drop is good:
+ if(updateAllowIndication(currentContainerVM, currentItem) === false) {
+ return;
+ }
+
+ let verticalDirection = scope.config.resolveVerticalDirection ? scope.config.resolveVerticalDirection({
+ containerElement: currentContainerElement,
+ containerRect: currentContainerRect,
+ item: currentItem,
+ element: currentElement,
+ elementRect: currentElementRect,
+ relatedElement: foundEl,
+ relatedRect: foundElDragRect,
+ placeholderIsInThisRow: placeholderIsInThisRow,
+ horizontalPlaceAfter: placeAfter
+ }) : true;
+
+ if (verticalDirection) {
+ placeAfter = (dragY > foundElDragRect.top + (foundElDragRect.height*.5));
+ }
+
+ if(verticalDirection) {
+
+ let el;
+ if(placeAfter === false) {
+ let lastLeft = foundElDragRect.left;
+ elementsInSameRow.findIndex((x) => {
+ if(x.dragRect.left < lastLeft) {
+ lastLeft = x.dragRect.left;
+ el = x.el;
+ }
+ });
+ } else {
+ let lastRight = foundElDragRect.right;
+ elementsInSameRow.findIndex((x) => {
+ if(x.dragRect.right > lastRight) {
+ lastRight = x.dragRect.right;
+ el = x.el;
+ }
+ });
+ }
+ if(el) {
+ foundEl = el;
+ }
+ }
+
+ const foundElIndex = orderedContainerElements.indexOf(foundEl);
+ const placeAt = (placeAfter ? foundElIndex+1 : foundElIndex);
+
+ move(orderedContainerElements, placeAt);
+
+ return;
+ }
+ // We skipped the above part cause we are above or below container:
+
+ // Indication if drop is good:
+ if(updateAllowIndication(currentContainerVM, currentItem) === false) {
+ return;
+ }
+
+ if(dragY < currentContainerRect.top) {
+ move(orderedContainerElements, 0);
+ } else if(dragY > currentContainerRect.bottom) {
+ move(orderedContainerElements, -1);
+ }
+ }
+
+ function move(orderedContainerElements, newElIndex) {
+
+ newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex;
+
+ const placeBeforeElement = orderedContainerElements[newElIndex];
+ if (placeBeforeElement) {
+ // We do not need to move this, if the element to be placed before is it self.
+ if(placeBeforeElement !== currentElement) {
+ currentContainerElement.insertBefore(currentElement, placeBeforeElement);
+ }
+ } else {
+ currentContainerElement.appendChild(currentElement);
+ }
+
+ if(config.onChange) {
+ config.onChange();
+ }
+ }
+
+
+ /** Removes an element from container and returns its items-data entry */
+ vm.getItemOfElement = function (element) {
+ if(!element) {
+ return null;
+ }
+ return scope.model.find(entry => config.compareElementToModel(element, entry));
+ }
+ vm.removeItem = function (item) {
+ if(!item) {
+ return null;
+ }
+ const oldIndex = scope.model.indexOf(item);
+ if(oldIndex !== -1) {
+ return scope.model.splice(oldIndex, 1)[0];
+ }
+ return null;
+ }
+
+ vm.hasOtherItemsThan = function(item) {
+ return scope.model.filter(x => x !== item).length > 0;
+ }
+
+ vm.sync = function(element, fromVm) {
+
+ const movingItem = fromVm.getItemOfElement(element);
+ if(!movingItem) {
+ console.error("Could not find item of sync item")
+ return false;
+ }
+ if(vm.notifyRequestDrop({item: movingItem}) === false) {
+ return false;
+ }
+ if(fromVm.removeItem(movingItem) === null) {
+ console.error("Sync could not remove item")
+ return false;
+ }
+
+ /** Find next element, to then find the index of that element in items-data, to use as a safe reference to where the item will go in our items-data.
+ * This enables the container to contain various other elements and as well having these elements change while sorting is occurring.
+ */
+
+ // find next valid element (This assumes the next element in DOM is presented in items-data, aka. only moving one item between each sync)
+ let nextEl;
+ let loopEl = element;
+ while((loopEl = loopEl.nextElementSibling)) {
+ if(loopEl.matches && loopEl.matches(config.itemSelector)) {
+ nextEl = loopEl;
+ break;
+ }
+ }
+
+ let newIndex = scope.model.length;
+ if(nextEl) {
+ // We had a reference element, we want to get the index of it.
+ // This is problem if a item is being moved forward?
+ newIndex = scope.model.findIndex(entry => config.compareElementToModel(nextEl, entry));
+ }
+
+ scope.model.splice(newIndex, 0, movingItem);
+
+ const eventData = {item: movingItem, fromController:fromVm, toController:vm};
+ if(fromVm !== vm) {
+ fromVm.notifySync(eventData);
+ }
+ vm.notifySync(eventData);
+
+ return true;
+ }
+
+
+
+
+
+
+
+ var _lastIndicationContainerVM = null;
+ function updateAllowIndication(contextVM, item) {
+
+ // Remove old indication:
+ if(_lastIndicationContainerVM !== null && _lastIndicationContainerVM !== contextVM) {
+ _lastIndicationContainerVM.notifyAllowed();
+ }
+ _lastIndicationContainerVM = contextVM;
+
+ if(contextVM.notifyRequestDrop({item: item}) === true) {
+ contextVM.notifyAllowed();
+ return true;
+ }
+
+ contextVM.notifyDisallowed();// This block is not accepted to we will indicate that its not allowed.
+ return false;
+ }
+ function removeAllowIndication() {
+
+ // Remove old indication:
+ if(_lastIndicationContainerVM !== null) {
+ _lastIndicationContainerVM.notifyAllowed();
+ }
+ _lastIndicationContainerVM = null;
+ }
+
+
+
+
+
+ let autoScrollRAF;
+ let autoScrollEl;
+ const autoScrollSensitivity = 50;
+ const autoScrollSpeed = 16;
+ let autoScrollX = 0;
+ let autoScrollY = 0;
+
+ function handleAutoScroll(clientX, clientY) {
+
+ let scrollRect = null;
+ if (scrollElement) {
+ autoScrollEl = scrollElement;
+ scrollRect = scrollElement.getBoundingClientRect();
+ } else {
+ autoScrollEl = document.scrollingElement || document.documentElement;
+ scrollRect = {top: 0,
+ left: 0,
+ bottom: window.innerHeight,
+ right: window.innerWidth,
+ height: window.innerHeight,
+ width: window.innerWidth
+ }
+ }
+
+ const scrollWidth = autoScrollEl.scrollWidth;
+ const scrollHeight = autoScrollEl.scrollHeight;
+ const canScrollX = scrollRect.width < scrollWidth;
+ const canScrollY = scrollRect.height < scrollHeight;
+ const scrollPosX = autoScrollEl.scrollLeft;
+ const scrollPosY = autoScrollEl.scrollTop;
+
+ cancelAnimationFrame(autoScrollRAF);
+
+ if(canScrollX || canScrollY) {
+
+ autoScrollX = (Math.abs(scrollRect.right - clientX) <= autoScrollSensitivity && scrollPosX + scrollRect.width < scrollWidth) - (Math.abs(scrollRect.left - clientX) <= autoScrollSensitivity && !!scrollPosX);
+ autoScrollY = (Math.abs(scrollRect.bottom - clientY) <= autoScrollSensitivity && scrollPosY + scrollRect.height < scrollHeight) - (Math.abs(scrollRect.top - clientY) <= autoScrollSensitivity && !!scrollPosY);
+ autoScrollRAF = requestAnimationFrame(performAutoScroll);
+ }
+ }
+ function performAutoScroll() {
+ autoScrollEl.scrollLeft += autoScrollX * autoScrollSpeed;
+ autoScrollEl.scrollTop += autoScrollY * autoScrollSpeed;
+ autoScrollRAF = requestAnimationFrame(performAutoScroll);
+ }
+ function stopAutoScroll() {
+ cancelAnimationFrame(autoScrollRAF);
+ autoScrollRAF = null;
+ }
+
+
+
+
+ vm.notifySync = function(data) {
+ if(config.onSync) {
+ config.onSync(data);
+ }
+ }
+ vm.notifyDisallowed = function() {
+ if(config.onDisallowed) {
+ config.onDisallowed();
+ }
+ }
+ vm.notifyAllowed = function() {
+ if(config.onAllowed) {
+ config.onAllowed();
+ }
+ }
+ vm.notifyRequestDrop = function(data) {
+ if(config.onRequestDrop) {
+ return config.onRequestDrop(data);
+ }
+ return true;
+ }
+
+
+
+ scope.$on('$destroy', () => {
+
+ if(currentElement) {
+ handleDragEnd()
+ }
+
+ _lastIndicationContainerVM = null;
+
+ containerEl['umbBlockGridSorter:vm'] = null
+ containerEl.removeEventListener('dragover', preventDragOver);
+
+ observer.disconnect();
+ observer = null;
+ containerEl = null;
+ scrollElement = null;
+ vm = null;
+ });
+ }
+
+ var directive = {
+ restrict: 'A',
+ scope: {
+ config: '=umbBlockGridSorter',
+ model: '=umbBlockGridSorterModel'
+ },
+ link: link
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbBlockGridSorter', UmbBlockGridSorter);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css
index a095fb438c..5dcc95a61d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css
@@ -1,14 +1,15 @@
/** Example of how a grid layout stylehseet could be done with Flex box: */
-
.umb-block-grid__layout-container {
position: relative;
display: flex;
flex-wrap: wrap;
+ gap: var(--umb-block-grid--row-gap, 0) var(--umb-block-grid--column-gap, 0);
}
.umb-block-grid__layout-item {
+ position: relative;
--umb-block-grid__layout-item-calc: calc(var(--umb-block-grid--item-column-span) / var(--umb-block-grid--grid-columns));
- width: calc(var(--umb-block-grid__layout-item-calc) * 100%);
+ width: calc(var(--umb-block-grid__layout-item-calc) * 100% - (1 - var(--umb-block-grid__layout-item-calc)) * var(--umb-block-grid--column-gap, 0px));
}
@@ -17,10 +18,12 @@
display: flex;
flex-wrap: wrap;
width: 100%;
+ gap: var(--umb-block-grid--areas-row-gap, 0) var(--umb-block-grid--areas-column-gap, 0);
}
.umb-block-grid__area {
+ position: relative;
--umb-block-grid__area-calc: calc(var(--umb-block-grid--area-column-span) / var(--umb-block-grid--area-grid-columns, 1));
- width: calc(var(--umb-block-grid__area-calc) * 100%);
+ width: calc(var(--umb-block-grid__area-calc) * 100% - (1 - var(--umb-block-grid__area-calc)) * var(--umb-block-grid--areas-column-gap, 0px));
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout.css b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout.css
index f6fe596b5c..60e0c4e96a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout.css
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout.css
@@ -29,13 +29,13 @@
.umb-block-grid__area {
position: relative;
/* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */
- grid-column-end: span min(calc(var(--umb-block-grid--item-column-span, 1) * 3), var(--umb-block-grid--grid-columns));
+ grid-column-end: span min(calc(var(--umb-block-grid--area-column-span, 1) * 3), var(--umb-block-grid--grid-columns));
grid-row: span var(--umb-block-grid--area-row-span, 1);
}
@media (min-width:1024px) {
.umb-block-grid__layout-item {
- grid-column-end: span var(--umb-block-grid--item-column-span, 1);
+ grid-column-end: span min(var(--umb-block-grid--item-column-span, 1), var(--umb-block-grid--grid-columns));
}
.umb-block-grid__area {
grid-column-end: span var(--umb-block-grid--area-column-span, 1);
diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js
index 89101d2f1d..6619754315 100644
--- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js
+++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js
@@ -29,7 +29,6 @@ module.exports = function (config) {
'lib/umbraco/Extensions.js',
'node_modules/lazyload-js/LazyLoad.min.js',
'node_modules/angular-dynamic-locale/dist/tmhDynamicLocale.min.js',
- 'node_modules/sortablejs/Sortable.min.js',
//app bootstrap and loader
'test/config/app.unit.js',