diff --git a/src/bundle/Resources/public/js/scripts/admin.contenttype.edit.js b/src/bundle/Resources/public/js/scripts/admin.contenttype.edit.js index e31220be1a..b77656f3e0 100644 --- a/src/bundle/Resources/public/js/scripts/admin.contenttype.edit.js +++ b/src/bundle/Resources/public/js/scripts/admin.contenttype.edit.js @@ -1,9 +1,8 @@ -(function (global, doc, ibexa, Routing, Translator) { +(function (global, doc, ibexa, Routing, Translator, bootstrap) { const SELECTOR_INPUTS_TO_VALIDATE = '.ibexa-input[required]:not([disabled]):not([hidden])'; - let targetContainer = null; + const TIMEOUT_REMOVE_HIGHLIGHT = 3000; let sourceContainer = null; let currentDraggedItem = null; - let draggedItemPosition = null; let isEditFormValid = false; const editForm = doc.querySelector('.ibexa-content-type-edit-form'); let inputsToValidate = editForm.querySelectorAll(SELECTOR_INPUTS_TO_VALIDATE); @@ -14,6 +13,7 @@ const filterFieldInput = doc.querySelector('.ibexa-available-field-types__sidebar-filter'); const popupMenuElement = sectionsNode.querySelector('.ibexa-popup-menu'); const addGroupTriggerBtn = sectionsNode.querySelector('.ibexa-content-type-edit__add-field-definitions-group-btn'); + const fieldDefinitionsGroups = doc.querySelectorAll('.ibexa-collapse--field-definitions-group'); const noFieldsAddedError = Translator.trans( /*@Desc("You have to add at least one field definition")*/ 'content_type.edit.error.no_added_fields_definition', {}, @@ -68,13 +68,11 @@ }); }; const removeDragPlaceholders = () => { - const placeholderNodes = doc.querySelectorAll( - '.ibexa-field-definitions-placeholder:not(.ibexa-field-definitions-placeholder--anchored)', - ); + const placeholderNodes = doc.querySelectorAll('.ibexa-field-definitions-placeholder'); placeholderNodes.forEach((placeholderNode) => placeholderNode.remove()); }; - const createFieldDefinitionNode = (fieldNode) => { + const createFieldDefinitionNode = (fieldNode, { targetContainer, draggedItemPosition }) => { let targetPlace = ''; const items = targetContainer.querySelectorAll('.ibexa-collapse'); @@ -86,19 +84,19 @@ } if (draggedItemPosition === -1) { - targetPlace = targetContainer.querySelector('.ibexa-field-definitions-placeholder--anchored'); + targetPlace = targetContainer.lastChild; } else if (draggedItemPosition === 0) { targetPlace = targetContainer.firstChild; } else { targetPlace = [...items].find((item, index) => index === draggedItemPosition); } - fieldNode.classList.add('ibexa-collapse--field-definition-highlight'); + fieldNode.classList.add('ibexa-collapse--field-definition-highlighted'); targetContainer.insertBefore(fieldNode, targetPlace); return fieldNode; }; - const attachFieldDefinitionNodeEvents = (fieldNode) => { + const attachFieldDefinitionNodeEvents = (fieldNode, { targetContainer }) => { const fieldGroupInput = fieldNode.querySelector('.ibexa-input--field-group'); const removeFieldsBtn = fieldNode.querySelectorAll('.ibexa-collapse__extra-action-button--remove-field-definitions'); const fieldInputsToValidate = fieldNode.querySelectorAll(SELECTOR_INPUTS_TO_VALIDATE); @@ -133,11 +131,11 @@ }), ); }; - const insertFieldDefinitionNode = (fieldNode) => { - const fieldDefinitionNode = createFieldDefinitionNode(fieldNode); + const insertFieldDefinitionNode = (fieldNode, { targetContainer, draggedItemPosition }) => { + const fieldDefinitionNode = createFieldDefinitionNode(fieldNode, { targetContainer, draggedItemPosition }); removeDragPlaceholders(); - attachFieldDefinitionNodeEvents(fieldDefinitionNode); + attachFieldDefinitionNodeEvents(fieldDefinitionNode, { targetContainer }); dispatchInsertFieldDefinitionNode(fieldDefinitionNode); return fieldDefinitionNode; @@ -171,11 +169,9 @@ groups.forEach((group) => { const groupFieldsDefinitionCount = group.querySelectorAll('.ibexa-collapse--field-definition').length; const emptyGroupPlaceholder = group.querySelector('.ibexa-field-definitions-empty-group'); - const anchoredPlaceholder = group.querySelector('.ibexa-field-definitions-placeholder--anchored'); const removeBtn = group.querySelector('.ibexa-collapse__extra-action-button--remove-field-definitions-group'); emptyGroupPlaceholder.classList.toggle('ibexa-field-definitions-empty-group--hidden', groupFieldsDefinitionCount !== 0); - anchoredPlaceholder.classList.toggle('ibexa-field-definitions-placeholder--hidden', groupFieldsDefinitionCount === 0); removeBtn.disabled = groupFieldsDefinitionCount > 0; }); @@ -187,12 +183,16 @@ }); doc.querySelectorAll('.ibexa-collapse--field-definition').forEach((fieldDefinition, index) => { - fieldDefinition.querySelector('.ibexa-input--position').value = index; + const positionInput = fieldDefinition.querySelector('.ibexa-input--position'); + + if (positionInput) { + fieldDefinition.querySelector('.ibexa-input--position').value = index; + } }); }; - const addField = () => { + const addField = ({ targetContainer, draggedItemPosition }) => { if (!sourceContainer.classList.contains('ibexa-available-field-types__list')) { - insertFieldDefinitionNode(currentDraggedItem); + insertFieldDefinitionNode(currentDraggedItem, { targetContainer, draggedItemPosition }); afterChangeGroup(); return; @@ -216,15 +216,19 @@ fetch(generateRequest('add', bodyData, languageCode)) .then(ibexa.helpers.request.getTextFromResponse) .then((response) => { - insertFieldDefinitionNode(response); + removeLoadingField(); + insertFieldDefinitionNode(response, { targetContainer, draggedItemPosition }); afterChangeGroup(); }) .catch(ibexa.helpers.notification.showErrorNotification); }; - const reorderFields = () => { - createFieldDefinitionNode(currentDraggedItem); + const reorderFields = ({ targetContainer, draggedItemPosition }) => { + createFieldDefinitionNode(currentDraggedItem, { targetContainer, draggedItemPosition }); removeDragPlaceholders(); + currentDraggedItem.classList.add('ibexa-collapse--field-definition-highlighted'); + currentDraggedItem.classList.add('ibexa-collapse--field-definition-loading'); + const fieldsOrder = [...doc.querySelectorAll('.ibexa-collapse--field-definition')].map( (fieldDefinition) => fieldDefinition.dataset.fieldDefinitionIdentifier, ); @@ -237,7 +241,10 @@ fetch(request) .then(ibexa.helpers.request.getTextFromResponse) - .then(() => afterChangeGroup()) + .then(() => { + currentDraggedItem.classList.remove('ibexa-collapse--field-definition-loading'); + afterChangeGroup(); + }) .catch(ibexa.helpers.notification.showErrorNotification); }; const removeFieldsGroup = (event) => { @@ -276,11 +283,21 @@ }, }; + collapseNode.classList.add('ibexa-collapse--field-definition-removing'); + bootstrap.Collapse.getOrCreateInstance(collapseNode.querySelector('.ibexa-collapse__body'), { + toggle: false, + }).hide(); + event.currentTarget.blur(); + fetch(generateRequest('remove', bodyData)) .then(ibexa.helpers.request.getTextFromResponse) .then(() => { - collapseNode.remove(); - afterChangeGroup(); + collapseNode.classList.add('ibexa-collapse--field-definition-remove-animation'); + + collapseNode.addEventListener('animationend', () => { + collapseNode.remove(); + afterChangeGroup(); + }); }) .catch(ibexa.helpers.notification.showErrorNotification); }; @@ -343,32 +360,76 @@ scrollToNode.scrollIntoView({ behavior: 'smooth' }); }; + const setActiveGroup = (group) => { + const currentActiveGroup = doc.querySelector( + '.ibexa-collapse--field-definitions-group.ibexa-collapse--active-field-definitions-group', + ); + + currentActiveGroup?.classList.remove('ibexa-collapse--active-field-definitions-group'); + group.classList.add('ibexa-collapse--active-field-definitions-group'); + }; + const removeLoadingField = () => { + const field = doc.querySelector('.ibexa-collapse--field-definition-loading'); + + field.remove(); + }; + const removeHighlight = () => { + const field = doc.querySelector('.ibexa-collapse--field-definition-highlighted'); + + field?.classList.remove('ibexa-collapse--field-definition-highlighted'); + }; class FieldDefinitionDraggable extends ibexa.core.Draggable { + constructor(config) { + super(config); + + this.emptyContainer = this.itemsContainer.querySelector('.ibexa-field-definitions-empty-group'); + + this.getPlaceholderNode = this.getPlaceholderNode.bind(this); + this.getPlaceholderPositionTop = this.getPlaceholderPositionTop.bind(this); + } + onDrop(event) { - targetContainer = event.currentTarget; - - const dragContainerItems = targetContainer.querySelectorAll( - '.ibexa-collapse--field-definition, .ibexa-field-definitions-placeholder:not(.ibexa-field-definitions-placeholder--anchored)', - ); - const currentActiveGroup = doc.querySelector( - '.ibexa-collapse--field-definitions-group.ibexa-collapse--active-field-definitions-group', - ); + const targetContainer = event.currentTarget; + const dragContainerItems = targetContainer.querySelectorAll('.ibexa-collapse--field-definition'); const targetContainerGroup = targetContainer.closest('.ibexa-collapse--field-definitions-group'); - - draggedItemPosition = [...dragContainerItems].findIndex((item, index, array) => { + const targetContainerList = targetContainerGroup.closest('.ibexa-content-type-edit__field-definitions-group-list'); + const fieldTemplate = targetContainerList.dataset.template; + const fieldRendered = fieldTemplate.replace('{{ type }}', currentDraggedItem.dataset.itemIdentifier); + let draggedItemPosition = [...dragContainerItems].findIndex((item, index, array) => { return item.classList.contains('ibexa-field-definitions-placeholder') && index < array.length - 1; }); + if (draggedItemPosition === -1) { + draggedItemPosition = targetContainer.querySelectorAll('.ibexa-collapse--field-definition').length; + } + if (sourceContainer.isEqualNode(targetContainer)) { - reorderFields(); + reorderFields({ targetContainer, draggedItemPosition }); } else { - addField(); + createFieldDefinitionNode(fieldRendered, { targetContainer, draggedItemPosition }); + addField({ targetContainer, draggedItemPosition }); } - currentActiveGroup?.classList.remove('ibexa-collapse--active-field-definitions-group'); - targetContainerGroup.classList.add('ibexa-collapse--active-field-definitions-group'); - + setActiveGroup(targetContainerGroup); removeDragPlaceholders(); + + global.setTimeout(() => { + removeHighlight(); + }, TIMEOUT_REMOVE_HIGHLIGHT); + } + + getPlaceholderNode(event) { + const draggableItem = super.getPlaceholderNode(event); + + if (draggableItem) { + return draggableItem; + } + + if (this.emptyContainer.contains(event.target)) { + return this.emptyContainer; + } + + return null; } onDragStart(event) { @@ -378,9 +439,39 @@ sourceContainer = currentDraggedItem.parentNode; } + onDragOver(event) { + const isDragSuccessful = super.onDragOver(event); + + if (!isDragSuccessful) { + return false; + } + + const item = this.getPlaceholderNode(event); + + if (item.isSameNode(this.emptyContainer)) { + this.emptyContainer.classList.toggle('ibexa-field-definitions-empty-group--hidden'); + } + + return true; + } + onDragEnd() { currentDraggedItem.style.removeProperty('display'); } + + init() { + super.init(); + + doc.body.addEventListener('dragover', (event) => { + if (!this.itemsContainer.contains(event.target)) { + const groupFieldsDefinitionCount = this.itemsContainer.querySelectorAll('.ibexa-collapse--field-definition').length; + + this.emptyContainer.classList.toggle('ibexa-field-definitions-empty-group--hidden', groupFieldsDefinitionCount !== 0); + } else { + event.preventDefault(); + } + }); + } } filterFieldInput.addEventListener('keyup', searchField, false); @@ -426,20 +517,32 @@ availableField.addEventListener( 'click', (event) => { - const activeTargetContainer = doc.querySelector( + const targetContainer = doc.querySelector( '.ibexa-collapse--field-definitions-group.ibexa-collapse--active-field-definitions-group .ibexa-content-type-edit__field-definition-drop-zone', ); - if (!activeTargetContainer) { + if (!targetContainer) { return; } currentDraggedItem = event.currentTarget; sourceContainer = currentDraggedItem.parentNode; - draggedItemPosition = -1; - targetContainer = activeTargetContainer; - addField(); + const draggedItemPosition = targetContainer.querySelectorAll('.ibexa-collapse--field-definition').length; + const targetContainerGroup = targetContainer.closest('.ibexa-collapse--field-definitions-group'); + const targetContainerList = targetContainerGroup.closest('.ibexa-content-type-edit__field-definitions-group-list'); + const fieldTemplate = targetContainerList.dataset.template; + const fieldRendered = fieldTemplate.replace('{{ type }}', currentDraggedItem.dataset.itemIdentifier); + + targetContainer + .querySelector('.ibexa-field-definitions-empty-group') + .classList.add('ibexa-field-definitions-empty-group--hidden'); + createFieldDefinitionNode(fieldRendered, { targetContainer, draggedItemPosition }); + addField({ targetContainer, draggedItemPosition }); + + global.setTimeout(() => { + removeHighlight(); + }, TIMEOUT_REMOVE_HIGHLIGHT); }, false, ); @@ -456,6 +559,7 @@ draggableGroups.push(draggable); }); + fieldDefinitionsGroups.forEach((group) => group.addEventListener('click', () => setActiveGroup(group), false)); inputsToValidate.forEach(attachValidateEvents); editForm.addEventListener( @@ -483,4 +587,4 @@ ); toggleAddGroupTriggerBtnState(); -})(window, window.document, window.ibexa, window.Routing, window.Translator); +})(window, window.document, window.ibexa, window.Routing, window.Translator, window.bootstrap); diff --git a/src/bundle/Resources/public/js/scripts/core/draggable.js b/src/bundle/Resources/public/js/scripts/core/draggable.js index 4236bc9500..4bcbfe5c9a 100644 --- a/src/bundle/Resources/public/js/scripts/core/draggable.js +++ b/src/bundle/Resources/public/js/scripts/core/draggable.js @@ -1,6 +1,7 @@ (function (global, doc, ibexa) { const SELECTOR_PLACEHOLDER = '.ibexa-draggable__placeholder'; const SELECTOR_PREVENT_DRAG = '.ibexa-draggable__prevent-drag'; + const TIMEOUT_REMOVE_HIGHLIGHT = 3000; class Draggable { constructor(config) { @@ -11,6 +12,12 @@ this.selectorItem = config.selectorItem; this.selectorPlaceholder = config.selectorPlaceholder || SELECTOR_PLACEHOLDER; this.selectorPreventDrag = config.selectorPreventDrag || SELECTOR_PREVENT_DRAG; + this.selectorItemContent = `${this.selectorItem}__content`; + this.itemMainClass = this.selectorItem.slice(1); + this.highlightClass = `${this.itemMainClass}--highlighted`; + this.draggingOutClass = `${this.itemMainClass}--is-dragging-out`; + this.removingClass = `${this.itemMainClass}--is-removing`; + this.removedClass = `${this.itemMainClass}--removed`; this.onDragStart = this.onDragStart.bind(this); this.onDragEnd = this.onDragEnd.bind(this); @@ -19,11 +26,26 @@ this.addPlaceholder = this.addPlaceholder.bind(this); this.removePlaceholder = this.removePlaceholder.bind(this); this.attachEventHandlersToItem = this.attachEventHandlersToItem.bind(this); + this.getPlaceholderNode = this.getPlaceholderNode.bind(this); + this.toggleNonInteractive = this.toggleNonInteractive.bind(this); + this.removeHighlight = this.removeHighlight.bind(this); + this.triggerHighlight = this.triggerHighlight.bind(this); } attachEventHandlersToItem(item) { item.ondragstart = this.onDragStart; item.ondragend = this.onDragEnd; + item.addEventListener('ibexa-drag-and-drop:start-removing', () => { + item.classList.add(this.removingClass); + }); + item.addEventListener('ibexa-drag-and-drop:end-removing', (event) => { + item.classList.add(this.removedClass); + + item.addEventListener('animationend', () => { + item.remove(); + event.detail.callback(); + }); + }); const preventedNode = item.querySelector(this.selectorPreventDrag); @@ -36,34 +58,86 @@ } } + triggerHighlight(item) { + item.classList.add(this.highlightClass); + + global.setTimeout(() => { + this.removeHighlight(); + }, TIMEOUT_REMOVE_HIGHLIGHT); + } + + removeHighlight() { + const highlightedItem = doc.querySelector(`.${this.highlightClass}`); + + highlightedItem?.classList.remove(this.highlightClass); + } + + getPlaceholderNode(event) { + const { target, clientY } = event; + + const itemNode = target.closest(`${this.selectorItem}:not(${this.selectorPlaceholder})`); + + if (itemNode) { + return itemNode; + } + + const items = [...this.itemsContainer.querySelectorAll(this.selectorItem)]; + items.reverse(); + + const insertAfterItem = items.find((item) => { + const { top } = item.getBoundingClientRect(); + + return top <= clientY; + }); + + return insertAfterItem; + } + + getPlaceholderPositionTop(item, event) { + return event.clientY; + } + + toggleNonInteractive(state) { + [...this.itemsContainer.querySelectorAll(this.selectorItem)].forEach((el) => { + el.classList.toggle(`${this.itemMainClass}--is-non-interactive`, state); + }); + } + onDragStart(event) { event.dataTransfer.dropEffect = 'move'; event.dataTransfer.setData('text/html', event.currentTarget); setTimeout(() => { - event.target.style.setProperty('display', 'none'); + event.target.closest(this.selectorItem).classList.add(this.draggingOutClass); + this.toggleNonInteractive(true); }, 0); this.draggedItem = event.currentTarget; } onDragEnd() { - this.draggedItem.style.removeProperty('display'); + this.itemsContainer.querySelector(`.${this.draggingOutClass}`).classList.remove(this.draggingOutClass); + this.toggleNonInteractive(false); } onDragOver(event) { - const item = event.target.closest(`${this.selectorItem}:not(${this.selectorPlaceholder})`); + const item = this.getPlaceholderNode(event); if (!item) { return false; } + const positionY = this.getPlaceholderPositionTop(item, event); + this.removePlaceholder(); - this.addPlaceholder(item, event.clientY); + this.addPlaceholder(item, positionY); + + return true; } onDrop() { this.itemsContainer.insertBefore(this.draggedItem, this.itemsContainer.querySelector(this.selectorPlaceholder)); this.removePlaceholder(); + this.triggerHighlight(this.draggedItem); } addPlaceholder(element, positionY) { diff --git a/src/bundle/Resources/public/scss/_content-type-edit.scss b/src/bundle/Resources/public/scss/_content-type-edit.scss index 1fa03882cc..7fb208aaf6 100644 --- a/src/bundle/Resources/public/scss/_content-type-edit.scss +++ b/src/bundle/Resources/public/scss/_content-type-edit.scss @@ -103,6 +103,7 @@ &__body-content { padding: 0 calculateRem(24px) calculateRem(20px); + min-height: calculateRem(410px); } } } @@ -118,6 +119,10 @@ border: calculateRem(1px) solid $ibexa-color-light; box-shadow: $ibexa-content-type-edit-field-shadow; + &:hover { + border-color: $ibexa-color-dark; + } + .ibexa-collapse { &__header { box-shadow: $ibexa-content-type-edit-field-shadow; @@ -139,8 +144,17 @@ margin-left: 0; } - &__extra-action-button--remove-field-definitions { - margin-left: auto; + &__toggle-btn, + &__extra-action-button { + &:hover { + .ibexa-icon { + fill: $ibexa-color-primary; + } + } + + &--remove-field-definitions { + margin-left: auto; + } } &__toggle-btn:not(.ibexa-collapse__toggle-btn--status) { @@ -174,6 +188,73 @@ } } + &--field-definition-removing { + border-color: $ibexa-color-light-300; + animation: field-remove-pulse 0.2s 1; + transform-origin: center; + pointer-events: none; + + &:hover { + border-color: $ibexa-color-light-300; + } + + .ibexa-collapse__header { + background-color: $ibexa-color-light-300; + } + + .ibexa-collapse__header-label { + color: $ibexa-color-light-500; + } + + .ibexa-collapse__draggable-btn .ibexa-icon, + .ibexa-icon { + fill: $ibexa-color-light-500; + } + } + + &--field-definition-remove-animation { + animation: remove-field 1s 1; + } + + &--field-definition-loading { + animation: field-add-pulse 1s 1; + pointer-events: none; + } + + &--field-definition-highlighted { + border-color: $ibexa-color-info; + + .ibexa-collapse__header { + background-color: $ibexa-color-info-100; + } + + .ibexa-collapse__header-label { + color: $ibexa-color-info; + } + + .ibexa-collapse__draggable-btn .ibexa-icon, + .ibexa-icon { + fill: $ibexa-color-info; + } + } + + &--field-definition-error { + border-color: $ibexa-color-danger; + + .ibexa-collapse__header { + background-color: $ibexa-color-danger-100; + } + + .ibexa-collapse__header-label { + color: $ibexa-color-danger; + } + + .ibexa-collapse__draggable-btn .ibexa-icon, + .ibexa-icon { + fill: $ibexa-color-danger; + } + } + &--field-definition.ibexa-collapse--collapsed { .ibexa-collapse__header { border-bottom: none; @@ -231,6 +312,57 @@ @include drag-item-placeholder; } +.ibexa-field-definitions-placeholder-full { + @include drag-item-placeholder-full; +} + .ibexa-field-definitions-empty-group { @include empty-drop-zone; } + +@keyframes remove-field { + 100% { + height: 0; + margin-bottom: 0; + opacity: 0; + transform: scale(0); + } +} + +@keyframes field-remove-pulse { + 0% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 0 $ibexa-color-light-300; + } + 10% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 0 $ibexa-color-light-300; + } + 20% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 calculateRem(10px) $ibexa-color-light-300; + } + 100% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 0 $ibexa-color-light-300; + } +} + +@keyframes field-add-pulse { + 0% { + border-color: $ibexa-color-info-100; + box-shadow: 0 0 0 0 $ibexa-color-info-100; + } + 10% { + border-color: $ibexa-color-info-100; + box-shadow: 0 0 0 0 $ibexa-color-info-100; + } + 20% { + border-color: $ibexa-color-info-100; + box-shadow: 0 0 0 calculateRem(10px) $ibexa-color-info-100; + } + 100% { + border-color: $ibexa-color-info-100; + box-shadow: 0 0 0 0 $ibexa-color-info-100; + } +} diff --git a/src/bundle/Resources/public/scss/mixins/_drag-and-drop.scss b/src/bundle/Resources/public/scss/mixins/_drag-and-drop.scss index cb10e6d6ff..fcd587d314 100644 --- a/src/bundle/Resources/public/scss/mixins/_drag-and-drop.scss +++ b/src/bundle/Resources/public/scss/mixins/_drag-and-drop.scss @@ -66,7 +66,6 @@ $self: &; height: auto; - overflow: hidden; &__title-bar { display: flex; @@ -98,26 +97,50 @@ &--collapsed { height: calculateRem(34px); + overflow: hidden; } } @mixin sidebar-drag-item { - display: flex; - width: 100%; + $self: &; + + background: $ibexa-color-light-300; margin-bottom: calculateRem(8px); - padding: calculateRem(13px) calculateRem(10px); - border: calculateRem(1px) solid $ibexa-color-light; border-radius: $ibexa-border-radius; - box-shadow: calculateRem(4px) calculateRem(22px) calculateRem(20px) calculateRem(-7px) rgba($ibexa-color-info, 0.05); - - &[draggable='true'] { - cursor: grab; - } + display: flex; + width: 100%; &:last-of-type { margin-bottom: calculateRem(24px); } + &:hover { + #{$self}__content { + border-color: $ibexa-color-dark; + transform: scale(1.02) translateX(-10px); + box-shadow: calculateRem(4px) calculateRem(10px) calculateRem(17px) 0 rgba($ibexa-color-info, 0.2); + } + + #{$self}__drag-icon { + fill: $ibexa-color-dark; + } + } + + &__content { + background: $ibexa-color-white; + display: flex; + width: 100%; + padding: calculateRem(13px) calculateRem(10px); + border: calculateRem(1px) solid $ibexa-color-light; + border-radius: $ibexa-border-radius; + box-shadow: calculateRem(4px) calculateRem(2px) calculateRem(17px) 0 rgba($ibexa-color-info, 0.05); + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; + + &[draggable='true'] { + cursor: grab; + } + } + &__drag { display: flex; align-items: center; @@ -147,25 +170,93 @@ display: none; } - &--unavailable, - &--is-dragging-out { - background: $ibexa-color-light-400; + &--unavailable { opacity: 0.5; + + &:hover { + #{$self}__content { + border-color: $ibexa-color-light; + background: $ibexa-color-light-400; + transform: none; + box-shadow: calculateRem(4px) calculateRem(2px) calculateRem(17px) 0 rgba($ibexa-color-info, 0.05); + } + } } - &:hover { - border-color: $ibexa-color-dark; + &--is-dragging-out { + #{$self}__content { + opacity: 0; + } } } @mixin drag-item { + $self: &; + display: flex; - width: 100%; - padding: calculateRem(12px) calculateRem(16px); - background-color: $ibexa-color-white; + background: $ibexa-color-light-300; + margin-bottom: calculateRem(8px); border-radius: $ibexa-border-radius; - border: calculateRem(1px) solid $ibexa-color-light; - box-shadow: calculateRem(4px) calculateRem(22px) calculateRem(20px) calculateRem(-7px) rgba($ibexa-color-info, 0.05); + + &:hover:not(&--is-non-interactive) { + #{$self}__content { + border-color: $ibexa-color-dark; + transform: scale(1.02) translateX(-10px); + box-shadow: calculateRem(4px) calculateRem(22px) calculateRem(47px) 0 rgba($ibexa-color-info, 0.2); + } + } + + &--is-dragging-out { + #{$self}__content { + opacity: 0; + } + } + + &--highlighted { + #{$self}__content { + border-color: $ibexa-color-info-300; + background-color: $ibexa-color-info-100; + animation: item-highlight-pulse 1s 1; + } + } + + &--is-removing { + #{$self}__content { + border-color: $ibexa-color-light-300; + animation: item-remove-pulse 0.2s 1; + transform-origin: center; + pointer-events: none; + + &:hover { + border-color: $ibexa-color-light-300; + } + } + } + + &--removed { + animation: remove-field 1s 1 forwards; + } + + &--error { + #{$self}__content { + border-color: $ibexa-color-danger; + } + } + + &__content { + display: flex; + background-color: $ibexa-color-white; + width: 100%; + padding: calculateRem(12px) calculateRem(16px); + border: calculateRem(1px) solid $ibexa-color-light; + border-radius: $ibexa-border-radius; + box-shadow: calculateRem(4px) calculateRem(2px) calculateRem(17px) 0 rgba($ibexa-color-info, 0.05); + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; + + &[draggable='true'] { + cursor: grab; + } + } &__left-col { display: flex; @@ -211,6 +302,22 @@ } @mixin drag-item-placeholder { + display: flex; + height: calculateRem(4px); + border-radius: calculateRem(2px); + background-color: $ibexa-color-info; + margin: 0 0 calculateRem(8px) 0; + + &--anchored { + background-color: $ibexa-color-light-300; + } + + &--hidden { + display: none; + } +} + +@mixin drag-item-placeholder-full { $self: &; display: flex; @@ -273,3 +380,54 @@ display: none; } } + +@keyframes remove-field { + 100% { + height: 0; + margin-bottom: 0; + opacity: 0; + transform: scale(0); + } +} + +@keyframes item-remove-pulse { + 0% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 0 $ibexa-color-light-300; + } + 10% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 0 $ibexa-color-light-300; + } + 20% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 calculateRem(10px) $ibexa-color-light-300; + } + 100% { + border-color: $ibexa-color-light-300; + box-shadow: 0 0 0 0 $ibexa-color-light-300; + } +} + +@keyframes item-highlight-pulse { + 0% { + border-color: $ibexa-color-info-300; + background-color: $ibexa-color-info-100; + box-shadow: 0 0 0 0 $ibexa-color-info-100; + } + 10% { + border-color: $ibexa-color-info-300; + background-color: $ibexa-color-info-100; + box-shadow: 0 0 0 0 $ibexa-color-info-100; + } + 20% { + border-color: $ibexa-color-info-300; + background-color: $ibexa-color-info-100; + box-shadow: 0 0 0 calculateRem(10px) $ibexa-color-info-100; + } + 100% { + border-color: $ibexa-color-info-300; + background-color: $ibexa-color-info-100; + box-shadow: calculateRem(4px) calculateRem(2px) calculateRem(17px) 0 rgba($ibexa-color-info, 0.05); + } +} diff --git a/src/bundle/Resources/views/themes/admin/content_type/available_field_types.html.twig b/src/bundle/Resources/views/themes/admin/content_type/available_field_types.html.twig index 182f780394..87039dcd1c 100644 --- a/src/bundle/Resources/views/themes/admin/content_type/available_field_types.html.twig +++ b/src/bundle/Resources/views/themes/admin/content_type/available_field_types.html.twig @@ -17,21 +17,25 @@ diff --git a/src/bundle/Resources/views/themes/admin/content_type/field_definition.html.twig b/src/bundle/Resources/views/themes/admin/content_type/field_definition.html.twig index b77ca74bbd..2e6507d799 100644 --- a/src/bundle/Resources/views/themes/admin/content_type/field_definition.html.twig +++ b/src/bundle/Resources/views/themes/admin/content_type/field_definition.html.twig @@ -5,7 +5,11 @@ {% if not field_definition.vars.disable_remove|default(false) %} {% set extra_actions = extra_actions|merge([ - { 'icon_name': 'trash', 'button_class': 'ibexa-collapse__extra-action-button--remove-field-definitions'} + { + 'icon_name': 'discard', + 'icon_size': 'tiny-small', + 'button_class': 'ibexa-collapse__extra-action-button--remove-field-definitions', + } ]) %} {% endif %} {%- embed "@ibexadesign/ui/component/collapse.html.twig" with { diff --git a/src/bundle/Resources/views/themes/admin/content_type/field_definitions.html.twig b/src/bundle/Resources/views/themes/admin/content_type/field_definitions.html.twig index 6f40df2dfe..bee0bef434 100644 --- a/src/bundle/Resources/views/themes/admin/content_type/field_definitions.html.twig +++ b/src/bundle/Resources/views/themes/admin/content_type/field_definitions.html.twig @@ -1,5 +1,26 @@
-
+ {% set extra_actions = [ + { + 'icon_name': 'discard', + 'icon_size': 'tiny-small', + 'button_class': 'ibexa-collapse__extra-action-button--remove-field-definitions', + } + ] %} + +
{% set should_show_first = true %} {% for field_defintions in grouped_field_defintions %} @@ -20,7 +41,11 @@ 'header_label': id | ibexa_field_group_name, 'is_expanded': true, 'extra_actions': [ - { 'icon_name': 'trash', 'button_class': 'ibexa-btn--ghost ibexa-collapse__extra-action-button--remove-field-definitions-group'} + { + 'icon_name': 'discard', + 'icon_size': 'tiny-small', + 'button_class': 'ibexa-btn--ghost ibexa-collapse__extra-action-button--remove-field-definitions-group', + } ], 'data_attr': { 'data-fields-group-id': id @@ -37,15 +62,6 @@ {% for field_definition in field_defintions %} {{ include('@ibexadesign/content_type/field_definition.html.twig', { is_draggable: is_draggable ?? true}) }} {% endfor %} - - {% set field_definitions_placeholder_class = [ - 'ibexa-field-definitions-placeholder--anchored', - not field_defintions|length ? 'ibexa-field-definitions-placeholder--hidden' - ] %} - - {%- include "@ibexadesign/content_type/field_definitions_placeholder.html.twig" with { - 'field_definitions_placeholder_class': field_definitions_placeholder_class|join(' ') - } -%} {% endblock %}
{% endblock %} diff --git a/src/bundle/Resources/views/themes/admin/content_type/field_definitions_placeholder.html.twig b/src/bundle/Resources/views/themes/admin/content_type/field_definitions_placeholder.html.twig index b95183b07a..c6dff64ddc 100644 --- a/src/bundle/Resources/views/themes/admin/content_type/field_definitions_placeholder.html.twig +++ b/src/bundle/Resources/views/themes/admin/content_type/field_definitions_placeholder.html.twig @@ -1,6 +1,8 @@ -
-
-
-
-
+{% set placeholder_class = full_placeholder|default(false) ? 'ibexa-field-definitions-placeholder-full' : 'ibexa-field-definitions-placeholder' %} + +
+
+
+
+
diff --git a/src/bundle/Resources/views/themes/admin/content_type/part/field_definition_form.html.twig b/src/bundle/Resources/views/themes/admin/content_type/part/field_definition_form.html.twig index a0ccb2ba8f..cd0decabe8 100644 --- a/src/bundle/Resources/views/themes/admin/content_type/part/field_definition_form.html.twig +++ b/src/bundle/Resources/views/themes/admin/content_type/part/field_definition_form.html.twig @@ -21,7 +21,8 @@ }, 'extra_actions': [ { - 'icon_name': 'trash', + 'icon_name': 'discard', + 'icon_size': 'tiny-small', 'button_class': 'ibexa-collapse__extra-action-button--remove-field-definitions', } ] diff --git a/src/bundle/Resources/views/themes/admin/ui/component/collapse.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/collapse.html.twig index d9adfa69e4..d3d5cabdc2 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/collapse.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/collapse.html.twig @@ -28,7 +28,7 @@ {% if extra_actions is defined %} {% for action in extra_actions %} diff --git a/src/lib/Behat/BrowserContext/ContentTypeContext.php b/src/lib/Behat/BrowserContext/ContentTypeContext.php index 6c99d524a9..f82014ea86 100644 --- a/src/lib/Behat/BrowserContext/ContentTypeContext.php +++ b/src/lib/Behat/BrowserContext/ContentTypeContext.php @@ -247,7 +247,7 @@ public function iCheckBlockInField($blockName) public function iCheckEditorLaunchModeOption(string $viewMode): void { $this->contentTypeUpdatePage->verifyIsLoaded(); - $this->contentTypeUpdatePage->expandLastFieldDefinition('fieldDefinitionOpenContainerEdit'); + $this->contentTypeUpdatePage->expandLastFieldDefinition(); $this->contentTypeUpdatePage->selectEditorLaunchMode($viewMode); } } diff --git a/src/lib/Behat/Page/ContentTypeUpdatePage.php b/src/lib/Behat/Page/ContentTypeUpdatePage.php index 4af26f0675..c1af2f9e34 100644 --- a/src/lib/Behat/Page/ContentTypeUpdatePage.php +++ b/src/lib/Behat/Page/ContentTypeUpdatePage.php @@ -9,6 +9,8 @@ namespace Ibexa\AdminUi\Behat\Page; use Ibexa\Behat\Browser\Element\Condition\ElementExistsCondition; +use Ibexa\Behat\Browser\Element\Condition\ElementsCountCondition; +use Ibexa\Behat\Browser\Element\Condition\ElementTransitionHasEndedCondition; use Ibexa\Behat\Browser\Element\Criterion\ElementAttributeCriterion; use Ibexa\Behat\Browser\Element\Criterion\ElementTextCriterion; use Ibexa\Behat\Browser\Element\Mapper\ElementTextMapper; @@ -18,61 +20,66 @@ class ContentTypeUpdatePage extends AdminUpdateItemPage { public function fillFieldDefinitionFieldWithValue(string $fieldName, string $label, string $value) { - $this->expandLastFieldDefinition('fieldDefinitionOpenContainer'); - $this->getHTMLPage()->find($this->getLocator('fieldDefinitionOpenContainer')) + $this->expandLastFieldDefinition(); + $this->getHTMLPage() + ->find($this->getLocator('fieldDefinitionOpenContainer')) ->findAll($this->getLocator('field'))->getByCriterion(new ElementTextCriterion($label)) ->find($this->getLocator('fieldInput')) ->setValue($value); } - public function expandLastFieldDefinition(string $locatorValue): void + public function expandLastFieldDefinition(): void { - $fieldToggleLocator = $this->getLocator('fieldDefinitionToggle'); - $lastFieldDefinition = $this->getHTMLPage()->find($fieldToggleLocator); + $fieldDefinitionLocator = new VisibleCSSLocator( + 'lastFieldDefinition', + 'div.ibexa-collapse__body-content div.ibexa-collapse--field-definition' + ); + $lastFieldDefinition = $this->getHTMLPage()->findAll($fieldDefinitionLocator)->last(); $lastFieldDefinition->mouseOver(); $lastFieldDefinition->assert()->isVisible(); $lastFieldDefinition->click(); - $this->getHTMLPage()->setTimeout(5) - ->waitUntilCondition(new ElementExistsCondition($this->getHTMLPage(), $this->getLocator($locatorValue))); + + $this->getHTMLPage()->waitUntilCondition(new ElementTransitionHasEndedCondition($lastFieldDefinition, new VisibleCSSLocator('transition', 'div'))); } public function specifyLocators(): array { return array_merge(parent::specifyLocators(), [ - new VisibleCSSLocator('fieldDefinitionContainer', '.ibexa-collapse--field-definition div.ibexa-collapse__header'), + new VisibleCSSLocator('fieldDefinition', '.ibexa-collapse--field-definition'), new VisibleCSSLocator('field', '.form-group'), new VisibleCSSLocator('contentTypeAddButton', '.ibexa-content-type-edit__add-field-definitions-group-btn'), new VisibleCSSLocator('contentTypeCategoryList', ' div.ibexa-content-type-edit__add-field-definitions-group > ul > li:nth-child(n):not(.ibexa-popup-menu__item-action--disabled)'), - new VisibleCSSLocator('availableFieldLabelList', '.ibexa-available-field-types__list > li'), - new VisibleCSSLocator('workspace', '#content_collapse > div.ibexa-collapse__body-content > div'), + new VisibleCSSLocator('availableFieldLabelList', '.ibexa-available-field-types__list > li:not(.ibexa-available-field-type--hidden)'), + new VisibleCSSLocator('workspace', '.ibexa-collapse__body-content'), new VisibleCSSLocator('fieldDefinitionToggle', '.ibexa-collapse:nth-last-child(2) > div.ibexa-collapse__header > button:last-child:not([data-bs-target="#content_collapse"])'), new VisibleCSSLocator('selectLaunchEditorMode', '.form-check .ibexa-input--radio'), new VisibleCSSLocator('fieldDefinitionOpenContainer', '[data-collapsed="false"] .ibexa-content-type-edit__field-definition-content'), - new VisibleCSSLocator('fieldDefinitionOpenContainerEdit', '#content_collapse > div > div[data-collapsed="false"]'), new VisibleCSSLocator('selectBlocksDropdown', '.ibexa-page-select-items__toggler'), + new VisibleCSSLocator('fieldDefinitionSearch', '.ibexa-available-field-types__sidebar-filter'), ]); } public function addFieldDefinition(string $fieldName) { - $availableFieldLabel = $this->getLocator('availableFieldLabelList'); - $listElement = $this->getHTMLPage() - ->findAll($availableFieldLabel) - ->getByCriterion(new ElementTextCriterion($fieldName)); - $listElement->mouseOver(); - - $fieldPosition = array_search( - $fieldName, - $this->getHTMLPage()->findAll($this->getLocator('availableFieldLabelList'))->mapBy(new ElementTextMapper()), - true - ) + 1; // CSS selectors are 1-indexed - - $availableFieldLabelsScript = "document.querySelector('.ibexa-available-field-types__list > li:nth-child(%d) > .ibexa-available-field-type__label')"; - $scriptToExecute = sprintf($availableFieldLabelsScript, $fieldPosition); - $this->getSession()->executeScript($scriptToExecute); + $currentFieldDefinitionCount = $this->getHTMLPage()->findAll($this->getLocator('fieldDefinition'))->count(); + $this->getHTMLPage()->find($this->getLocator('fieldDefinitionSearch'))->setValue($fieldName); + $fieldPosition = array_search($fieldName, $this->getHTMLPage()->findAll($this->getLocator('availableFieldLabelList'))->mapBy(new ElementTextMapper()), true) + 1; + $fieldSelector = new VisibleCSSLocator( + 'field', + sprintf( + '.ibexa-available-field-types__list > li:not(.ibexa-available-field-type--hidden) .ibexa-available-field-type__content:nth-of-type(%d)', + $fieldPosition + ) + ); + $this->getHTMLPage()->find($fieldSelector)->mouseOver(); + $this->getHTMLPage()->setTimeout(3)->waitUntilCondition(new ElementTransitionHasEndedCondition($this->getHTMLPage(), $fieldSelector)); + + $fieldScript = sprintf("document.querySelector('%s')", $fieldSelector->getSelector()); + $workspaceScript = sprintf("document.querySelector('%s')", $this->getLocator('workspace')->getSelector()); + $this->getHTMLPage()->dragAndDrop($fieldScript, $workspaceScript, $workspaceScript); + + $this->getHTMLPage()->setTimeout(3)->waitUntilCondition(new ElementsCountCondition($this->getHTMLPage(), $this->getLocator('fieldDefinition'), $currentFieldDefinitionCount + 1)); - $workspace = sprintf('document.querySelector(\'%s\')', $this->getLocator('workspace')->getSelector()); - $this->getHTMLPage()->dragAndDrop($scriptToExecute, $workspace, $workspace); usleep(1500000); //TODO: add proper wait condition }