Block Grid Editor sorting directive (#13391)

* make sure area border is on top of block views.

* rename class to avoid confusion

* change witch UI goes on top on hover

* Description informing all blocks are allowed when none is configured.

* add 'When empty'

* Sort mode

* ability to switch out property actions

* enter and exit sortmode from property actions

* gridsortblock

* rename block class to use sortblock

* Sort mode styling

* remove unused css selector

* fixing style for inline-creat button to appear above and not when hovering contextbar

* work on block grid inline editor

* use uui-button + enable installing demo blocks when its not the first dataType of this kind.

* improvements to inline editing POC

* update title of area config overlay editor

* reset columnSpan if no column span options is defined.

* Inline editing

* remove html comment

* remove code for transfer of stylesheets

* ability to hide label from directive

* inline editing using slots to render the umb-property in light dom

* remove property editor proxies when moving a block to a new area/block/context

* minor adjustments to custom views

* use individual slots for each area.

* Inline editing

* a little smaller rte min-height

* fire Custom focus/blur event for Block Grid Editor to catch for focus imitation

* disable inline editing prevalue field when custom view is set

* Fix scroll parent block into view

* initial work on sorter directive

* remove mediaBlock controller

* initial notes and structure

* further concept work

* remove consol log

* CSS for getting bigger areas

* removal of the forceLeft/forceRight code

* proven concept

* fix grid space detection. vertical/horizontal

* clean up and notes

* move into inner containers as well

* use last available index pr default

* boundary selector, for improved choise of dropping into an area

* hide last inline create button when dragging around

* remove console.log

* removal of forced placement in css

* default config and clean up

* notes

* bring back removed code

* show area ui when in dragging mode

* more specific selector

* drop allowance + clean up

* notes and clean up

* auto scroll

* turn --umb-block-grid--dragging-mode into conditional CSS Custom Property

* auto scroll

* refactoring

* fix condition mistake

* scope.config.resolveVerticalDirection

* wrap up simple setDragImage solution

* bring back vm.notifyVisualUpdate and clean up

* make draggableSelector optional, fallback to element

* implement umb-block-grid-sorter for Area PreValue editor

* remove sortableJS dependency

* remove sortableJs from dependencies

* wups, bring back the comma

* removed sortablejs from package-lock

* finished implementation of sorter for PreValue Block Areas

* fix for FireFox shadowDom issue, contains temprorary code.

* stop auto scroll

* make full thing dragable

* fix firefox issue (applying translateZ)

* comment

* make block fit in context columns

* revert element to where it came from if sync could not succeed + clean up

* ensure block does not push the amount of columns, this occourse when dragging item around.

* take horizontalPlaceAfter into account

* implement horizontalPlaceAfter in Areas Prevalue editor

* clean up dependencies

* Shift related el to first in row or last in row when there is no horizontal room

* clean up and correct calculation

* remove unused attribute

* revert to using el.getBoundingClientRect(), as the config.draggableSelector is not available for the placeholder item.

* bind model via dedicated binding to ensure it stay connected with the source model

* Update src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.html

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

* fix eslint issues

* ensure missingColumnWidth is above 0

* Do not allow dragging something thats not found in the model.

* remove as this is not an error.

* update to Flexbox solution

* as the complex model does not change we can use single way binding

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
Niels Lyngsø
2022-11-21 10:41:57 +01:00
committed by GitHub
parent c2140cc042
commit b698e4754d
21 changed files with 1040 additions and 533 deletions

View File

@@ -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"
}
];

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
}
</style>
@@ -100,6 +104,6 @@
<slot name="{{vm.propertySlotName}}"></slot>
<umb-block-grid-render-area-slots></umb-block-grid-render-area-slots>
<umb-block-grid-render-area-slots ng-if="block.layout.areas.length > 0"></umb-block-grid-render-area-slots>
</div>

View File

@@ -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;

View File

@@ -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);

View File

@@ -241,7 +241,6 @@
infiniteMode: true,
noTemplate: true,
isElement: true,
noTemplate: true,
submit: function (model) {
loadElementTypes().then( function () {
callback(model.documentTypeKey);

View File

@@ -93,7 +93,7 @@
var elementTypeId = elementType.id;
const editor = {
id: elementTypeId,
submit: function (model) {
submit: function () {
editorService.close();
},
close: function () {

View File

@@ -2,33 +2,40 @@
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<div ng-show="vm.loading !== true" class="umb-block-grid-area-editor__grid-wrapper" style="--umb-block-grid--block-grid-columns: {{vm.block.areaGridColumns || vm.rootLayoutColumns}}">
<div ng-if="vm.loading === false"
class="umb-block-grid-area-editor__grid-wrapper"
style="--umb-block-grid--block-grid-columns: {{vm.block.areaGridColumns || vm.rootLayoutColumns}}"
umb-block-grid-sorter="::vm.sorterOptions"
umb-block-grid-sorter-model="vm.model">
<umb-block-grid-configuration-area-entry ng-repeat="area in vm.model track by area.key"
class="umb-block-grid-area-editor__area"
ng-class="{'--isOpen':vm.openArea === area}"
area="area"
data-area-key="{{area.key}}"
data-col-span="{{area.columnSpan}}"
data-row-span="{{area.rowSpan}}"
style="
--umb-block-grid--grid-column: {{area.columnSpan}};
--umb-block-grid--area-column-span: {{area.columnSpan}};
--umb-block-grid--area-row-span: {{area.rowSpan}};
"
area="area"
ng-click="vm.editArea(area)"
on-edit="vm.editArea(area)"
on-delete="vm.requestDeleteArea(area)"
>
</umb-block-grid-configuration-area-entry>
<button
type="button"
ng-disabled="vm.disabled"
class="btn-reset umb-block-grid-area-editor__create-button umb-outline"
ng-click="vm.onNewAreaClick()">
<localize key="general_add">Add</localize>
</button>
</div>
<button
type="button"
ng-disabled="vm.disabled"
class="btn-reset umb-block-grid-area-editor__create-button umb-outline"
ng-click="vm.onNewAreaClick()">
<localize key="general_add">Add</localize>
</button>
</div>

View File

@@ -217,6 +217,9 @@ Grid part:
display: flex;
align-items: center;
justify-content: center;
height:50px;
width:100%;
color: @ui-action-discreet-type;
font-weight: bold;

View File

@@ -27,7 +27,7 @@
}
});
function BlockGridAreaAllowanceController($scope, $element, assetsService, localizationService, editorService) {
function BlockGridAreaAllowanceController($scope) {
var unsubscribe = [];

View File

@@ -1,6 +1,44 @@
(function () {
"use strict";
// Utils:
function getInterpolatedIndexOfPositionInWeightMap(target, weights) {
const map = [0];
weights.reduce((a, b, i) => { return map[i+1] = a+b; }, 0);
const foundValue = map.reduce((a, b) => {
let aDiff = Math.abs(a - target);
let bDiff = Math.abs(b - target);
if (aDiff === bDiff) {
return a < b ? a : b;
} else {
return bDiff < aDiff ? b : a;
}
})
const foundIndex = map.indexOf(foundValue);
const targetDiff = (target-foundValue);
let interpolatedIndex = foundIndex;
if (targetDiff < 0 && foundIndex === 0) {
// Don't adjust.
} else if (targetDiff > 0 && foundIndex === map.length-1) {
// Don't adjust.
} else {
const foundInterpolationWeight = weights[targetDiff >= 0 ? foundIndex : foundIndex-1];
interpolatedIndex += foundInterpolationWeight === 0 ? interpolatedIndex : (targetDiff/foundInterpolationWeight)
}
return interpolatedIndex;
}
function getAccumulatedValueOfIndex(index, weights) {
let i = 0, len = Math.min(index, weights.length), calc = 0;
while(i<len) {
calc += weights[i++];
}
return calc;
}
function TransferProperties(fromObject, toObject) {
for (var p in fromObject) {
toObject[p] = fromObject[p];
@@ -22,7 +60,7 @@
controller: BlockGridAreaController,
controllerAs: "vm",
bindings: {
model: "=",
model: "<",
block: "<",
allBlockTypes: "<",
allBlockGroups: "<",
@@ -34,7 +72,7 @@
}
});
function BlockGridAreaController($scope, $element, assetsService, localizationService, editorService, overlayService) {
function BlockGridAreaController($scope, localizationService, editorService, overlayService) {
var unsubscribe = [];
@@ -45,46 +83,79 @@
vm.$onInit = function() {
vm.rootLayoutColumns = vm.gridColumns;
assetsService.loadJs('lib/sortablejs/Sortable.min.js', $scope).then(onLoaded);
};
function onLoaded() {
vm.loading = false;
initializeSortable();
}
vm.loading = false;
};
function initializeSortable() {
function _sync(evt) {
const oldIndex = evt.oldIndex,
newIndex = evt.newIndex;
vm.model.splice(newIndex, 0, vm.model.splice(oldIndex, 1)[0]);
vm.sorterOptions = {
resolveVerticalDirection: resolveVerticalDirection,
compareElementToModel: (el, modelEntry) => 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);
}
}

View File

@@ -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();
}
});*/
}
})();

View File

@@ -2,9 +2,10 @@
<div
class="umb-block-grid__layout-container"
ng-class="{
'--not-allowing-drop': vm.showNotAllowedUI,
'--droppable-indication': vm.droppableIndication
'--not-allowing-drop': vm.showNotAllowedUI
}"
umb-block-grid-sorter="::vm.sorterOptions"
umb-block-grid-sorter-model="vm.entries"
>
<umb-block-grid-entry ng-repeat="layoutEntry in vm.entries track by layoutEntry.$block.key"
class="umb-block-grid__layout-item"

View File

@@ -139,7 +139,7 @@
createFlow: false
};
vm.sortMode = false;
vm.sortModeView = DefaultViewFolderPath + "gridsortblock/gridsortblock.editor.html";;
vm.sortModeView = DefaultViewFolderPath + "gridsortblock/gridsortblock.editor.html";
localizationService.localizeMany(["grid_addElement", "content_createEmpty", "blockEditor_addThis"]).then(function (data) {
vm.labels.grid_addElement = data[0];
@@ -166,16 +166,16 @@
$element[0].addEventListener("UmbBlockGrid_RemoveProperty", vm.onRemoveProxyProperty);
//listen for form validation changes
vm.valFormManager.onValidationStatusChanged(function (evt, args) {
vm.valFormManager.onValidationStatusChanged(function () {
vm.showValidation = vm.valFormManager.showValidation;
});
//listen for the forms saving event
unsubscribe.push($scope.$on("formSubmitting", function (ev, args) {
unsubscribe.push($scope.$on("formSubmitting", function () {
vm.showValidation = true;
}));
//listen for the forms saved event
unsubscribe.push($scope.$on("formSubmitted", function (ev, args) {
unsubscribe.push($scope.$on("formSubmitted", function () {
vm.showValidation = false;
}));
@@ -266,13 +266,13 @@
// Create Model Object, to manage our data for this Block Editor.
modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
$q.all([modelObject.load(), assetsService.loadJs('lib/sortablejs/Sortable.min.js', $scope)]).then(onLoaded);
modelObject.load().then(onLoaded);
};
// Called when we save the value, the server may return an updated data and our value is re-synced
// we need to deal with that here so that our model values are all in sync so we basically re-initialize.
function onServerValueChanged(newVal, oldVal) {
function onServerValueChanged(newVal) {
// We need to ensure that the property model value is an object, this is needed for modelObject to receive a reference and keep that updated.
if (typeof newVal !== 'object' || newVal === null) {// testing if we have null or undefined value or if the value is set to another type than Object.
@@ -757,7 +757,7 @@
function deleteAllBlocks() {
while(vm.layout.length) {
deleteBlock(vm.layout[0].$block);
};
}
}
function activateBlock(blockObject) {
@@ -777,8 +777,8 @@
*/
var wasNotActiveBefore = blockObject.active !== true;
// don't open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings.
// don't open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings.
if (openSettings !== true && blockObject.hideContentInOverlay === true) {
return;
}
@@ -875,7 +875,7 @@
}
vm.requestShowClipboard = requestShowClipboard;
function requestShowClipboard(parentBlock, areaKey, createIndex, mouseEvent) {
function requestShowClipboard(parentBlock, areaKey, createIndex) {
showCreateDialog(parentBlock, areaKey, createIndex, true);
}
@@ -983,7 +983,7 @@
}
};
blockPickerModel.clickClearClipboard = function ($event) {
blockPickerModel.clickClearClipboard = function () {
clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, availableContentTypesAliases);
clipboardService.clearEntriesOfType(clipboardService.TYPES.BLOCK, availableContentTypesAliases);
};
@@ -994,7 +994,7 @@
// open block picker overlay
editorService.open(blockPickerModel);
};
}
function userFlowWhenBlockWasCreated(parentBlock, areaKey, createIndex) {
var blockObject;
@@ -1045,7 +1045,7 @@
vm.clipboardItems.push(pasteEntry);
});
var entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases);
entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases);
entriesForPaste.forEach(function (entry) {
var pasteEntry = {
type: clipboardService.TYPES.BLOCK,
@@ -1126,7 +1126,7 @@
localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function (localizedLabel) {
clipboardService.copyArray(clipboardService.TYPES.BLOCK, aliases, elementTypesToCopy, localizedLabel, contentNodeIcon || "icon-thumbnail-list", vm.model.id);
});
};
}
function gatherNestedBlocks(block) {
const nested = [];
@@ -1348,14 +1348,14 @@
vm.startDraggingMode = startDraggingMode;
function startDraggingMode() {
document.documentElement.style.setProperty("--umb-block-grid--dragging-mode", 1);
document.documentElement.style.setProperty("--umb-block-grid--dragging-mode", ' ');
firstLayoutContainer.style.minHeight = firstLayoutContainer.getBoundingClientRect().height + "px";
}
vm.exitDraggingMode = exitDraggingMode;
function exitDraggingMode() {
document.documentElement.style.setProperty("--umb-block-grid--dragging-mode", 0);
document.documentElement.style.setProperty("--umb-block-grid--dragging-mode", 'initial');
firstLayoutContainer.style.minHeight = "";
}

View File

@@ -2,6 +2,7 @@
"use strict";
// Utils:
function getInterpolatedIndexOfPositionInWeightMap(target, weights) {
const map = [0];
@@ -30,9 +31,12 @@
return interpolatedIndex;
}
function isWithinRect(x, y, rect, modifier) {
return (x > 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<len) {
calc += weights[i++];
}
return calc;
}
@@ -64,22 +68,22 @@
}
);
function BlockGridEntriesController($element, $scope, $timeout) {
function BlockGridEntriesController($element, $scope) {
const unsubscribe = [];
const vm = this;
vm.showNotAllowedUI = false;
vm.invalidAmount = false;
vm.areaConfig = null;
vm.locallyAvailableBlockTypes = 0;
vm.invalidBlockTypes = [];
vm.movingLayoutEntry = null;
vm.layoutColumnsInt = 0;
vm.containedPropertyEditorProxies = [];
vm.showNotAllowedUI = false;
let currentContainedPropertyEditorProxies = [];
vm.$onInit = function () {
initializeSortable();
initializeSorter();
if(vm.parentBlock) {
vm.areaConfig = vm.parentBlock.config.areas.find(area => 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();
}
});
};
});
}
})();

View File

@@ -159,7 +159,7 @@
$scope.$evalAsync();
}
unsubscribe.push($scope.$watch("depth", (newVal, oldVal) => {
unsubscribe.push($scope.$watch("depth", () => {
vm.childDepth = parseInt(vm.depth) + 1;
}));

View File

@@ -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);
})();

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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',