Merge branch 'release/11.0' into v11/merge_release_into_dev

# Conflicts:
#	src/JsonSchema/AppSettings.cs
#	src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css
#	version.json
This commit is contained in:
nikolajlauridsen
2022-11-22 15:30:45 +01:00
61 changed files with 1464 additions and 1135 deletions

View File

@@ -291,11 +291,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": "6.2.0",
"typeahead.js": "0.11.1",
@@ -15132,11 +15131,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",
@@ -28990,11 +28984,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": "6.2.0",
"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);
@@ -222,8 +223,8 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
.umb-block-grid__block--context {
position: absolute;
top: -20px;
right: 0;
top: -21px;
right: -1px;
font-size: 12px;
z-index: 4;
display: var(--umb-block-grid--block-ui-display, flex);
@@ -244,6 +245,9 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
padding-top: 1px;
background-color: #3544B1;
color: white;
border-top: rgba(255, 255, 255, .7) 1px solid;
border-left: rgba(255, 255, 255, .7) 1px solid;
border-right: rgba(255, 255, 255, .7) 1px solid;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
display: inline-block;
@@ -366,6 +370,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
padding: 0;
background-color: white;
border: @blueDark solid 1px;
box-shadow: 0 0 0 1px rgba(255, 255, 255, .7);
opacity: 0;
transition: opacity 120ms;
}
@@ -412,15 +417,17 @@ 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;
width: 100%;
width: var(--umb-block-grid-editor--inline-create-width, 100%);
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;
}
@@ -428,7 +435,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
/* If at root, and full-width then become 40px wider: */
--calc: clamp(0, calc(var(--umb-block-grid--item-column-span) - (var(--umb-block-grid--grid-columns)-1)), 1);
left: calc(-20px * var(--calc));
width: calc(100% + 40px * var(--calc));
width: calc(var(--umb-block-grid-editor--inline-create-width, 100%) + 40px * var(--calc));
}
.umb-block-grid__block--inline-create-button.--after {
@@ -454,6 +461,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 +564,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 +602,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 +615,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 +627,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 +635,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;
@@ -630,7 +648,12 @@ 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 +677,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 +696,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;
@@ -740,4 +765,4 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
color: @errorText;
border-radius: 0;
border-top: 1px solid rgba(255, 255, 255, .2);
}
}

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

@@ -13,7 +13,7 @@
</umb-editor-header>
<umb-editor-container>
<div class="form-vertical umb-block-grid-block-configuration-layout">
<div class="form-vertical">
<div class="umb-group-panel">

View File

@@ -38,12 +38,12 @@
"groupKey": null
};
function BlockConfigurationController($scope, $element, $http, elementTypeResource, overlayService, localizationService, editorService, eventsService, udiService, dataTypeResource, umbRequestHelper) {
var unsubscribe = [];
var vm = this;
const vm = this;
vm.openBlock = null;
vm.showSampleDataCTA = false;
@@ -57,6 +57,7 @@
if (blockGroupModel.value == null) {
blockGroupModel.value = [];
}
vm.blockGroups = blockGroupModel.value;
if (!$scope.model.value) {
@@ -72,12 +73,10 @@
});
loadElementTypes();
}
function loadElementTypes() {
return elementTypeResource.getAll().then(function (elementTypes) {
return elementTypeResource.getAll().then(elementTypes => {
vm.elementTypes = elementTypes;
});
}
@@ -90,13 +89,14 @@
}
}
}
unsubscribe.push(eventsService.on("editors.documentType.saved", updateUsedElementTypes));
function removeReferencesToElementTypeKey(contentElementTypeKey) {
// Clean up references to this one:
$scope.model.value.forEach(blockType => {
blockType.areas.forEach(area => {
area.specifiedAllowance = area.specifiedAllowance?.filter(allowance =>
area.specifiedAllowance = area.specifiedAllowance?.filter(allowance =>
allowance.elementTypeKey !== contentElementTypeKey
) || [];
});
@@ -107,7 +107,7 @@
// Clean up references to this one:
$scope.model.value.forEach(blockType => {
blockType.areas.forEach(area => {
area.specifiedAllowance = area.specifiedAllowance?.filter(allowance =>
area.specifiedAllowance = area.specifiedAllowance?.filter(allowance =>
allowance.groupKey !== groupKey
) || [];
});
@@ -134,27 +134,29 @@
vm.removeBlockByIndex = function (index) {
const blockType = $scope.model.value[index];
if(blockType) {
if (blockType) {
$scope.model.value.splice(index, 1);
removeReferencesToElementTypeKey(blockType.contentElementTypeKey);
}
};
const defaultOptions = {
axis: '',
tolerance: "pointer",
opacity: 0.7,
scroll: true
};
vm.groupSortableOptions = {
...defaultOptions,
...defaultOptions,
axis: 'y',
handle: '.__handle',
items: ".umb-block-card-group",
cursor: "grabbing",
placeholder: 'umb-block-card-group --sortable-placeholder'
};
vm.blockSortableOptions = {
...defaultOptions,
...defaultOptions,
"ui-floating": true,
connectWith: ".umb-block-card-grid",
items: "umb-block-card",
@@ -170,7 +172,6 @@
}
};
vm.getAvailableElementTypes = function () {
return vm.elementTypes.filter(function (type) {
return !$scope.model.value.find(function (entry) {
@@ -201,18 +202,18 @@
if (node.metaData.isElement === true) {
var key = udiService.getKey(node.udi);
// If a Block with this ElementType as content already exists, we will emit it as a possible option.
return $scope.model.value.find(function (entry) {
return $scope.model.value.find(function(entry) {
return key === entry.contentElementTypeKey;
});
}
return true;
},
filterCssClass: "not-allowed",
select: function (node) {
select: function(node) {
vm.addBlockFromElementTypeKey(udiService.getKey(node.udi), groupKey);
editorService.close();
},
close: function () {
close: function() {
editorService.close();
},
extraActions: [
@@ -241,14 +242,13 @@
infiniteMode: true,
noTemplate: true,
isElement: true,
noTemplate: true,
submit: function (model) {
loadElementTypes().then( function () {
submit: function(model) {
loadElementTypes().then(function() {
callback(model.documentTypeKey);
});
editorService.close();
},
close: function () {
close: function() {
editorService.close();
}
};
@@ -262,24 +262,19 @@
$scope.model.value.push(blockType);
vm.openBlockOverlay(blockType);
};
vm.openBlockOverlay = function (block, openAreas) {
var elementType = vm.getElementTypeByKey(block.contentElementTypeKey);
if(elementType) {
if (elementType) {
localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [elementType.name]).then(function (data) {
var clonedBlockData = Utilities.copy(block);
vm.openBlock = block;
var overlayModel = {
const overlayModel = {
block: clonedBlockData,
allBlockTypes: $scope.model.value,
allBlockGroups: vm.blockGroups,
@@ -288,11 +283,11 @@
title: data,
openAreas: openAreas,
view: "views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html",
size: "large",
size: "medium",
submit: function(overlayModel) {
loadElementTypes()// lets load elementType again, to ensure we are up to date.
TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.)
overlayModel.close();
},
close: function() {
@@ -306,14 +301,24 @@
});
} else {
alert("Cannot be edited cause ElementType does not exist.");
const overlay = {
close: () => {
overlayService.close()
}
};
localizationService.localize("blockEditor_elementTypeDoesNotExist").then(data => {
overlay.content = data;
overlayService.open(overlay);
});
}
};
vm.requestRemoveGroup = function(blockGroup) {
if(blockGroup.key) {
if (blockGroup.key) {
localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockGroupMessage", "blockEditor_confirmDeleteBlockGroupNotice"]).then(function (data) {
overlayService.confirmDelete({
title: data[0],
@@ -332,7 +337,7 @@
return false;
} else {
return true;
return true;
}
}
);
@@ -355,10 +360,8 @@
}
}
dataTypeResource.getAll().then(function(dataTypes) {
if(dataTypes.filter(x => x.alias === "Umbraco.BlockGrid").length === 0) {
if (dataTypes.filter(x => x.alias === "Umbraco.BlockGrid").length === 0) {
vm.showSampleDataCTA = true;
}
});
@@ -379,7 +382,7 @@
};
vm.blockGroups.push(sampleGroup);
}
function initSampleBlock(udi, groupKey, options) {
const key = udiService.getKey(udi);
if ($scope.model.value.find(X => X.contentElementTypeKey === key) === undefined) {
@@ -387,7 +390,7 @@
$scope.model.value.push(blockType);
}
}
initSampleBlock(data.umbBlockGridDemoHeadlineBlock, sampleGroup.key, {"label": "Headline ({{headline | truncate:true:36}})", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoHeadlineBlock.html"});
initSampleBlock(data.umbBlockGridDemoImageBlock, sampleGroup.key, {"label": "Image", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoImageBlock.html"});
initSampleBlock(data.umbBlockGridDemoRichTextBlock, sampleGroup.key, { "label": "Rich Text ({{richText | ncRichText | truncate:true:36}})", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoRichTextBlock.html"});
@@ -412,10 +415,10 @@
}
];
initSampleBlock(data.umbBlockGridDemoTwoColumnLayoutBlock, sampleGroup.key, {"label": "Two Column Layout", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoTwoColumnLayoutBlock.html", "allowInAreas": false, "areas": twoColumnLayoutAreas});
vm.showSampleDataCTA = false;
});
});
}

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

@@ -15,9 +15,9 @@
<umb-editor-container>
<div ng-if="vm.activeTab.alias === 'block'" class="form-vertical umb-block-grid-block-configuration-layout">
<div ng-if="vm.activeTab.alias === 'block'" class="form-vertical">
<div class="umb-group-panel umb-block-grid-block-configuration__umb-group-panel">
<div class="umb-group-panel">
<div class="umb-group-panel__header">
<localize key="general_General">General</localize>
@@ -80,124 +80,9 @@
</div>
<div class="umb-group-panel umb-block-grid-block-configuration__umb-group-panel">
<div class="umb-group-panel__header">
<div>
<localize key="blockEditor_sizeOptions">Size options</localize>
<umb-property-info-button button-title-key="general_readMore">
<localize key="blockEditor_sizeOptionsHelp">Define one or more size options, this enables resizing of the Block</localize>
</umb-property-info-button>
</div>
</div>
<div class="umb-group-panel__content">
<button ng-if="!vm.showSizeOptions" type="button" class="btn-reset __settings-input --noValue umb-outline" style="height: 80px;" ng-click="vm.showSizeOptions = true">
<localize key="blockEditor_showSizeOptions">Show size options</localize>
</button>
<!-- Column span options -->
<div ng-if="vm.showSizeOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="addNewColumnSpan"><localize key="blockEditor_allowedBlockColumns">Available column spans</localize></label>
<umb-property-info-button button-title-key="general_readMore">
<localize key="blockEditor_allowedBlockColumnsHelp">Define the different number of layout columns this block is allowed to span across.</localize>
</umb-property-info-button>
<div class="controls">
<umb-block-grid-column-editor model="vm.block.columnSpanOptions" block="vm.block" grid-columns="model.gridColumns"></umb-block-grid-column-editor>
</div>
</div>
</div>
<!-- min/max Row span options -->
<div ng-if="vm.showSizeOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="label"><localize key="blockEditor_allowedBlockRows">Available row spans</localize></label>
<umb-property-info-button button-title-key="general_readMore">
<localize key="blockEditor_allowedBlockRowsHelp">Define the range of layout rows this block is allowed to span across.</localize>
</umb-property-info-button>
<div class="controls">
<umb-property-editor name="rowMinMaxModel" model="vm.rowMinMaxModel" is-pre-value="true"></umb-property-editor>
</div>
</div>
</div>
</div>
</div>
<div class="umb-group-panel umb-block-grid-block-configuration__umb-group-panel">
<div class="umb-group-panel__header">
<localize key="blockEditor_headlineCatalogueAppearance">Catalogue appearance</localize>
</div>
<div class="umb-group-panel__content">
<button ng-if="!vm.showAppearanceOptions" type="button" class="btn-reset __settings-input --noValue umb-outline" style="height: 80px;" ng-click="vm.showAppearanceOptions = true">
<localize key="blockEditor_showAppearanceOptions">Show catalogue appearance</localize>
</button>
<!-- backgroundColor -->
<div ng-if="vm.showAppearanceOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="backgroundColor"><localize key="blockEditor_labelBackgroundColor">Background Color</localize></label>
<div class="controls">
<umb-color-picker
ng-model="vm.block.backgroundColor"
options="vm.colorPickerOptions"
on-change="vm.changeBackgroundColor(color)">
</umb-color-picker>
</div>
</div>
</div>
<!-- iconColor -->
<div ng-if="vm.showAppearanceOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="iconColor"><localize key="blockEditor_labelIconColor">Icon Color</localize></label>
<div class="controls">
<umb-color-picker
ng-model="vm.block.iconColor"
options="vm.colorPickerOptions"
on-change="vm.changeIconColor(color)">
</umb-color-picker>
</div>
</div>
</div>
<!-- thumbnail -->
<div ng-if="vm.showAppearanceOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="iconcolor"><localize key="blockEditor_thumbnail">Thumbnail</localize></label>
<div class="controls">
<div class="__settings-input --hasValue" ng-if="vm.block.thumbnail !== null" >
<umb-node-preview icon="'icon-document'" name="vm.block.thumbnail"></umb-node-preview>
<div class="__control-actions">
<button type="button" class="btn-reset __control-actions-btn --remove umb-outline" ng-click="vm.removeThumbnailForBlock(vm.block)">
<umb-icon icon="icon-wrong" class="icon"></umb-icon>
</button>
</div>
</div>
<button type="button" class="btn-reset __settings-input --noValue --hideText umb-outline" ng-if="vm.block.thumbnail === null" ng-click="vm.addThumbnailForBlock(vm.block)">
<localize key="blockEditor_addThumbnail">Add thumbnail</localize>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="umb-group-panel umb-block-grid-block-configuration__umb-group-panel">
<div class="umb-group-panel">
<div class="umb-group-panel__header">
<localize key="blockEditor_headlineAllowance">Permissions</localize>
@@ -244,11 +129,61 @@
</div>
<div class="umb-group-panel">
<div class="umb-group-panel__header">
<div>
<localize key="blockEditor_sizeOptions">Size options</localize>
<umb-property-info-button button-title-key="general_readMore">
<localize key="blockEditor_sizeOptionsHelp">Define one or more size options, this enables resizing of the Block</localize>
</umb-property-info-button>
</div>
</div>
<div class="umb-group-panel__content">
<button ng-if="!vm.showSizeOptions" type="button" class="btn-reset __settings-input --noValue umb-outline" style="height: 80px;" ng-click="vm.showSizeOptions = true">
<localize key="blockEditor_showSizeOptions">Show size options</localize>
</button>
<!-- Column span options -->
<div ng-if="vm.showSizeOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="addNewColumnSpan"><localize key="blockEditor_allowedBlockColumns">Available column spans</localize></label>
<umb-property-info-button button-title-key="general_readMore">
<localize key="blockEditor_allowedBlockColumnsHelp">Define the different number of layout columns this block is allowed to span across.</localize>
</umb-property-info-button>
<div class="controls">
<umb-block-grid-column-editor model="vm.block.columnSpanOptions" block="vm.block" grid-columns="model.gridColumns"></umb-block-grid-column-editor>
</div>
</div>
</div>
<!-- min/max Row span options -->
<div ng-if="vm.showSizeOptions" class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="label"><localize key="blockEditor_allowedBlockRows">Available row spans</localize></label>
<umb-property-info-button button-title-key="general_readMore">
<localize key="blockEditor_allowedBlockRowsHelp">Define the range of layout rows this block is allowed to span across.</localize>
</umb-property-info-button>
<div class="controls">
<umb-property-editor name="rowMinMaxModel" model="vm.rowMinMaxModel" is-pre-value="true"></umb-property-editor>
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-if="vm.activeTab.alias === 'areas'">
<div class="umb-group-panel umb-block-grid-block-configuration__umb-group-panel --span-two-cols">
<div class="umb-group-panel">
<div class="umb-group-panel__header">
@@ -288,7 +223,7 @@
</div>
<div ng-if="vm.activeTab.alias === 'advance'">
<div class="umb-group-panel umb-block-grid-block-configuration__umb-group-panel">
<div class="umb-group-panel">
<div class="umb-group-panel__header">
<localize key="blockEditor_headlineAdvanced">Advanced</localize>
@@ -384,6 +319,66 @@
</div>
</div>
<div class="umb-group-panel">
<div class="umb-group-panel__header">
<localize key="blockEditor_headlineCatalogueAppearance">Catalogue appearance</localize>
</div>
<div class="umb-group-panel__content">
<!-- backgroundColor -->
<div class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="backgroundColor"><localize key="blockEditor_labelBackgroundColor">Background Color</localize></label>
<div class="controls">
<umb-color-picker
ng-model="vm.block.backgroundColor"
options="vm.colorPickerOptions"
on-change="vm.changeBackgroundColor(color)">
</umb-color-picker>
</div>
</div>
</div>
<!-- iconColor -->
<div class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="iconColor"><localize key="blockEditor_labelIconColor">Icon Color</localize></label>
<div class="controls">
<umb-color-picker
ng-model="vm.block.iconColor"
options="vm.colorPickerOptions"
on-change="vm.changeIconColor(color)">
</umb-color-picker>
</div>
</div>
</div>
<!-- thumbnail -->
<div class="control-group umb-control-group -no-border">
<div class="umb-el-wrap">
<label class="control-label" for="iconcolor"><localize key="blockEditor_thumbnail">Thumbnail</localize></label>
<div class="controls">
<div class="__settings-input --hasValue" ng-if="vm.block.thumbnail !== null" >
<umb-node-preview icon="'icon-document'" name="vm.block.thumbnail"></umb-node-preview>
<div class="__control-actions">
<button type="button" class="btn-reset __control-actions-btn --remove umb-outline" ng-click="vm.removeThumbnailForBlock(vm.block)">
<umb-icon icon="icon-wrong" class="icon"></umb-icon>
</button>
</div>
</div>
<button type="button" class="btn-reset __settings-input --noValue --hideText umb-outline" ng-if="vm.block.thumbnail === null" ng-click="vm.addThumbnailForBlock(vm.block)">
<localize key="blockEditor_addThumbnail">Add thumbnail</localize>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</umb-editor-container>
@@ -413,4 +408,4 @@
</umb-editor-footer>
</umb-editor-view>
</form>
</div>
</div>

View File

@@ -1,28 +1,5 @@
.umb-block-grid-block-configuration-overlay {
.umb-block-grid-block-configuration-layout {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-gap: 0 20px;
grid-auto-flow: row;
grid-auto-rows: minmax(50px, auto);
}
.umb-block-grid-block-configuration__umb-group-panel {
@media (max-width: 1024px) {
grid-column: span 2;
}
&.--span-two-cols {
grid-column: span 2;
}
&.--span-two-rows {
grid-row: span 2;
}
}
.umb-node-preview {
flex-grow: 1;

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

@@ -18,7 +18,7 @@
<div class="__border"></div>
<button type="button" class="btn-reset" ng-click="vm.onClickAdd()">
<span class="sr-only">
<localize key="TODO spell out the action">Add</localize>
<localize key="blockEditor_addColumnSpanOption" tokens="[vm.columnSpanOption.columnSpan]">Add span option</localize>
{{vm.columnSpanOption.columnSpan}}
</span>
</button>

View File

@@ -4,18 +4,18 @@
<div class="umb-block-grid__area--actions">
<button type="button" class="btn-reset umb-outline action --edit" localize="title" title="TODO"
<button type="button" class="btn-reset umb-outline action --edit" localize="title" title="blockEditor_configureArea"
ng-click="vm.onEditClick($event);">
<umb-icon icon="icon-edit" class="icon"></umb-icon>
<span class="sr-only">
<localize key="general_edit">Edit</localize>
<localize key="blockEditor_configureArea">Edit</localize>
</span>
</button>
<button type="button" class="btn-reset umb-outline action --delete" localize="title" title="TODO"
<button type="button" class="btn-reset umb-outline action --delete" localize="title" title="blockEditor_deleteArea"
ng-click="vm.onDeleteClick($event);">
<umb-icon icon="icon-trash" class="icon"></umb-icon>
<span class="sr-only">
<localize key="general_delete">Delete</localize>
<localize key="blockEditor_deleteArea">Delete</localize>
</span>
</button>
</div>

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);
}
}
@@ -93,7 +164,6 @@
}
vm.requestDeleteArea = function (area) {
// TODO: Translations
localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockAreaMessage", "blockEditor_confirmDeleteBlockAreaNotice"]).then(function (data) {
overlayService.confirmDelete({
title: data[0],
@@ -145,8 +215,6 @@
vm.openArea = null;
vm.openAreaOverlay = function (area) {
// TODO: use the right localization key:
localizationService.localize("blockEditor_blockConfigurationOverlayTitle").then(function (localized) {
var clonedAreaData = Utilities.copy(area);

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,7 @@
"use strict";
/**
*
* Note for new backoffice: there is a lot of similarities between the Area configuration and the Block entry, as they both share Grid scaling features.
* TODO: Can we already as part of this PR make it shared as a dictionary or something?
*/
@@ -100,6 +98,13 @@
function updateGridLayoutData() {
if(!layoutContainer) {
layoutContainer = $element[0].closest('.umb-block-grid-area-editor__grid-wrapper');
if(!layoutContainer) {
console.error($element[0], 'could not find area-container');
}
}
const computedStyles = window.getComputedStyle(layoutContainer);
gridColumns = computedStyles.gridTemplateColumns.trim().split("px").map(x => Number(x));
@@ -126,12 +131,6 @@
window.addEventListener('mouseup', vm.onMouseUp);
window.addEventListener('mouseleave', vm.onMouseUp);
layoutContainer = $element[0].closest('.umb-block-grid-area-editor__grid-wrapper');
if(!layoutContainer) {
console.error($element[0], 'could not find area-container');
}
updateGridLayoutData();
scaleBoxBackdropEl = document.createElement('div');
@@ -217,6 +216,8 @@
vm.scaleHandlerKeyUp = function($event) {
updateGridLayoutData();
let addCol = 0;
let addRow = 0;
@@ -236,7 +237,7 @@
}
// Todo: Ensure value fit with configuration.
vm.area.columnSpan = Math.max(vm.area.columnSpan + addCol, 1);
vm.area.columnSpan = Math.min(Math.max(vm.area.columnSpan + addCol, 1), gridColumns.length);
vm.area.rowSpan = Math.max(vm.area.rowSpan + addRow, 1);
$event.originalEvent.stopPropagation();

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

@@ -1,7 +1,7 @@
<uui-button-inline-create
ng-if="!vm.blockEditorApi.readonly && !vm.hideInlineCreateAbove"
class="umb-block-grid__block--inline-create-button --above"
style="width: {{vm.inlineCreateAboveWidth}};"
style="--umb-block-grid-editor--inline-create-width: {{vm.inlineCreateAboveWidth}};"
ng-class="{'--at-root': vm.depth === '0'}"
ng-click="vm.clickInlineCreateAbove()"
ng-mouseover="vm.mouseOverInlineCreate()"

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];
@@ -9,7 +10,7 @@
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 {
@@ -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;
}
@@ -43,7 +47,7 @@
* @description
* renders all blocks for a given list for the block grid editor
*/
angular
.module("umbraco")
.component("umbBlockGridEntries", {
@@ -64,40 +68,36 @@
}
);
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);
}
vm.locallyAvailableBlockTypes = vm.blockEditorApi.internal.getAllowedTypesOf(vm.parentBlock, vm.areaKey);
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) {
var isMinRequirementGood = vm.entries.length >= vm.areaConfig.minAllowed;
vm.entriesForm.areaMinCount.$setValidity("areaMinCount", isMinRequirementGood);
@@ -126,12 +126,12 @@
'maxRequirement': maxAllowed
});
}
} else
} else
// For specific elementTypes:
if(allowance.elementTypeKey) {
const amount = vm.entries.filter(entry => entry.$block.data.contentTypeKey === allowance.elementTypeKey).length;
if(amount < minAllowed || (maxAllowed > 0 && amount > maxAllowed)) {
vm.invalidBlockTypes.push({
'key': allowance.elementTypeKey,
@@ -153,254 +153,41 @@
}
}
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);
}
vm.showNotAllowed = function() {
vm.showNotAllowedUI = true;
$scope.$evalAsync();
}
vm.hideNotAllowed = function() {
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 initializeSortable() {
function resolveVerticalDirection(data) {
const gridLayoutContainerEl = $element[0].querySelector('.umb-block-grid__layout-container');
var _lastIndicationContainerVM = null;
/** 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);
var targetRect = null;
var relatedEl = null;
var ghostEl = null;
var ghostRect = null;
var dragX = 0;
var dragY = 0;
var dragOffsetX = 0;
const foundElColumns = parseInt(data.relatedElement.dataset.colSpan, 10);
const currentElementColumns = data.item.columnSpan;
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);
if(currentElementColumns >= gridColumnNumber) {
return true;
}
// 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);
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,
@@ -409,184 +196,127 @@
const amountOfUnknownColumns = gridColumnNumber-amountOfColumnsInWeightMap;
if(amountOfUnknownColumns > 0) {
let accumulatedValue = getAccumulatedValueOfIndex(amountOfColumnsInWeightMap, approvedContainerGridColumns) || 0;
const layoutWidth = approvedContainerRect.width;
const missingColumnWidth = (layoutWidth-accumulatedValue)/amountOfUnknownColumns;
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 = foundRelatedElRect.left - approvedContainerRect.left;
const relatedStartX = Math.max(data.relatedRect.left - data.containerRect.left + offsetPlacement, 0);
const relatedStartCol = Math.round(getInterpolatedIndexOfPositionInWeightMap(relatedStartX, approvedContainerGridColumns));
if(relatedStartCol + relatedColumns + ghostColumns > gridColumnNumber) {
verticalDirection = true;
}
if (verticalDirection) {
placeAfter = (dragY > foundRelatedElRect.top + (foundRelatedElRect.height*.5));
}
// 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);
}
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);
}
function initializeSorter() {
vm.sorterOptions = {
ownerVM: vm,
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,
onContainerChange: onSortContainerChange,
onSync: onSortSync,
onDisallowed: onSortDisallowed,
onAllowed: onSortAllowed,
onRequestDrop: onSortRequestDrop
}
}
var rqaId = null
function _onDragMove(evt) {
var currentItemColumnSpanTarget;
function onSortStart(data) {
currentItemColumnSpanTarget = data.item.columnSpan;
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) {
// 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();
}
if(dragX === clientX && dragY === clientY) {
return;
}
dragX = clientX;
dragY = clientY;
ghostRect = ghostEl.getBoundingClientRect();
function onSortEnd() {
vm.blockEditorApi.internal.exitDraggingMode();
currentContainedPropertyEditorProxies = [];
notifyVisualUpdate();
$scope.$evalAsync();
}
const insideGhost = isWithinRect(dragX, dragY, ghostRect, 0);
if (!insideGhost) {
if(rqaId === null) {
rqaId = requestAnimationFrame(_moveGhostElement);
}
function getColumnSpanForContext(currentColumnSpan, columnSpanOptions, contextColumns) {
if (columnSpanOptions.length > 0) {
const availableOptions = columnSpanOptions.filter(option => option.columnSpan <= contextColumns);
if(availableOptions.length > 0) {
const closestColumnSpan = availableOptions.map(x => x.columnSpan).reduce(
(prev, curr) => {
return Math.abs(curr - currentColumnSpan) < Math.abs(prev - currentColumnSpan) ? curr : prev
}, 99999
);
if(closestColumnSpan) {
return closestColumnSpan;
}
}
}
return contextColumns;
}
vm.sortGroupIdentifier = "BlockGridEditor_"+vm.blockEditorApi.internal.uniqueEditorKey;
function onSortContainerChange(data) {
const contextColumns = vm.blockEditorApi.internal.getContextColumns(data.ownerVM.parentBlock, data.ownerVM.areaKey);
data.item.columnSpan = getColumnSpanForContext(currentItemColumnSpanTarget, data.item.$block.config.columnSpanOptions, contextColumns);
}
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,
function onSortSync(data) {
if (data.fromController !== data.toController) {
removeAllContainedPropertyEditorProxies();
}
$scope.$evalAsync();
vm.blockEditorApi.internal.setDirty();
}
scrollSensitivity: 50,
scrollSpeed: 16,
scroll: true,
forceAutoScrollFallback: true,
function onSortDisallowed() {
vm.showNotAllowedUI = true;
$scope.$evalAsync();
}
function onSortAllowed() {
vm.showNotAllowedUI = false;
$scope.$evalAsync();
}
function onSortRequestDrop(data) {
return vm.blockEditorApi.internal.isElementTypeKeyAllowedAt(vm.parentBlock, vm.areaKey, data.item.$block.config.contentElementTypeKey);
}
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();
}
});
};
$scope.$on('$destroy', function () {
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,717 @@
(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;
vm.ownerVM = config.ownerVM || null;
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.
currentContainerVM = vm;
if (config.onContainerChange) {
config.onContainerChange({item: currentItem, element: currentElement, ownerVM: currentContainerVM.ownerVM});
}
// 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);
}
}
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;
if (config.onContainerChange) {
config.onContainerChange({item: currentItem, element: currentElement, ownerVM: currentContainerVM.ownerVM});
}
}
}
}
// 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;
if (config.onContainerChange) {
config.onContainerChange({item: currentItem, element: currentElement, ownerVM: currentContainerVM.ownerVM});
}
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({element: currentElement, item: currentItem, ownerVM: currentContainerVM.ownerVM});
}
}
/** 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,13 +18,15 @@
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));
}
.umb-block-grid__actions {
clear: both;
}
}

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