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:
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
11
src/Umbraco.Web.UI.Client/package-lock.json
generated
11
src/Umbraco.Web.UI.Client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -241,7 +241,6 @@
|
||||
infiniteMode: true,
|
||||
noTemplate: true,
|
||||
isElement: true,
|
||||
noTemplate: true,
|
||||
submit: function (model) {
|
||||
loadElementTypes().then( function () {
|
||||
callback(model.documentTypeKey);
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
var elementTypeId = elementType.id;
|
||||
const editor = {
|
||||
id: elementTypeId,
|
||||
submit: function (model) {
|
||||
submit: function () {
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
function BlockGridAreaAllowanceController($scope, $element, assetsService, localizationService, editorService) {
|
||||
function BlockGridAreaAllowanceController($scope) {
|
||||
|
||||
var unsubscribe = [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});*/
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
$scope.$evalAsync();
|
||||
}
|
||||
unsubscribe.push($scope.$watch("depth", (newVal, oldVal) => {
|
||||
unsubscribe.push($scope.$watch("depth", () => {
|
||||
vm.childDepth = parseInt(vm.depth) + 1;
|
||||
}));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
})();
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user