diff --git a/core/src/main/resources/hudson/tools/InstallSourceProperty/config.jelly b/core/src/main/resources/hudson/tools/InstallSourceProperty/config.jelly index d9d8012101e1..f9f0b8275825 100644 --- a/core/src/main/resources/hudson/tools/InstallSourceProperty/config.jelly +++ b/core/src/main/resources/hudson/tools/InstallSourceProperty/config.jelly @@ -28,7 +28,7 @@ THE SOFTWARE. diff --git a/core/src/main/resources/hudson/tools/ToolInstallation/global.jelly b/core/src/main/resources/hudson/tools/ToolInstallation/global.jelly index 317b0dd48151..d903402f34a4 100644 --- a/core/src/main/resources/hudson/tools/ToolInstallation/global.jelly +++ b/core/src/main/resources/hudson/tools/ToolInstallation/global.jelly @@ -38,7 +38,7 @@ THE SOFTWARE. - + diff --git a/core/src/main/resources/lib/form/hetero-list.jelly b/core/src/main/resources/lib/form/hetero-list.jelly index b817c936c4e8..e20d9a85c47f 100644 --- a/core/src/main/resources/lib/form/hetero-list.jelly +++ b/core/src/main/resources/lib/form/hetero-list.jelly @@ -81,27 +81,30 @@ THE SOFTWARE. - - + + - - + + + + - ${descriptor.displayName} + ${descriptor.displayName} - + - - - - + + + + + + + - - @@ -120,7 +123,7 @@ THE SOFTWARE. - + @@ -153,10 +156,8 @@ THE SOFTWARE. - - ${attrs.addCaption?:'%Add'} - - + ${attrs.addCaption?:'%Add'} + diff --git a/core/src/main/resources/lib/form/repeatable.jelly b/core/src/main/resources/lib/form/repeatable.jelly index 8df3d44a4c61..d2ef5c80d658 100644 --- a/core/src/main/resources/lib/form/repeatable.jelly +++ b/core/src/main/resources/lib/form/repeatable.jelly @@ -136,10 +136,12 @@ THE SOFTWARE. ${header} - - - - + + + + + + @@ -153,7 +155,9 @@ THE SOFTWARE. ${header} - + + + @@ -165,12 +169,14 @@ THE SOFTWARE. ${header} - + + + - + ${attrs.add?:'%Add'} diff --git a/core/src/main/resources/lib/form/repeatable/repeatable.js b/core/src/main/resources/lib/form/repeatable/repeatable.js index 3f19dcfb7e7f..6f3b9dc6ac04 100644 --- a/core/src/main/resources/lib/form/repeatable/repeatable.js +++ b/core/src/main/resources/lib/form/repeatable/repeatable.js @@ -40,8 +40,6 @@ var repeatableSupport = { addOnTop = false; } - // importNode isn't supported in IE. - // nc = document.importNode(node,true); var nc = document.createElement("div"); nc.className = "repeated-chunk fade-in"; nc.setAttribute("name", this.name); @@ -59,7 +57,6 @@ var repeatableSupport = { registerSortableDragDrop(nc); } - nc.classList.remove("fade-in"); Behaviour.applySubtree(nc, true); this.update(); }, @@ -101,13 +98,13 @@ var repeatableSupport = { parentOfButton.insertBefore(addTopButton, parentOfButton.firstChild); Behaviour.applySubtree(addTopButton, true); } - children[0].className = "repeated-chunk first last only"; + children[0].className = "repeated-chunk fade-in first last only"; } else { - children[0].className = "repeated-chunk first"; + children[0].className = "repeated-chunk first fade-in"; for (var i = 1; i < children.length - 1; i++) { - children[i].className = "repeated-chunk middle"; + children[i].className = "repeated-chunk middle fade-in"; } - children[children.length - 1].className = "repeated-chunk last"; + children[children.length - 1].className = "repeated-chunk last fade-in"; } } }, @@ -133,6 +130,7 @@ var repeatableSupport = { // transition end not triggered in tests n.ontransitionend.call(n, {}); } + n.style.maxHeight = n.offsetHeight + "px"; n.classList.add("fade-out"); setTimeout(() => { diff --git a/core/src/main/resources/lib/form/repeatableDeleteButton.jelly b/core/src/main/resources/lib/form/repeatableDeleteButton.jelly index 2a1a97df4b22..41ccdf28026b 100644 --- a/core/src/main/resources/lib/form/repeatableDeleteButton.jelly +++ b/core/src/main/resources/lib/form/repeatableDeleteButton.jelly @@ -32,7 +32,10 @@ THE SOFTWARE. - + + + + ${deleteText} diff --git a/core/src/main/resources/lib/form/repeatableProperty.jelly b/core/src/main/resources/lib/form/repeatableProperty.jelly index e94c0b4c983d..7f913deb2caa 100644 --- a/core/src/main/resources/lib/form/repeatableProperty.jelly +++ b/core/src/main/resources/lib/form/repeatableProperty.jelly @@ -77,9 +77,7 @@ THE SOFTWARE. - - - - + + diff --git a/src/main/js/components/dropdowns/hetero-list.js b/src/main/js/components/dropdowns/hetero-list.js index 0eab2b9ea324..3d8a4684d8dd 100644 --- a/src/main/js/components/dropdowns/hetero-list.js +++ b/src/main/js/components/dropdowns/hetero-list.js @@ -151,7 +151,6 @@ function generateButtons() { } Behaviour.applySubtree(nc, true); ensureVisible(nc); - nc.classList.remove("fade-in"); layoutUpdateCallback.call(); }, true, diff --git a/src/main/js/sortable-drag-drop.js b/src/main/js/sortable-drag-drop.js index e056919c24b5..5724538dbadd 100644 --- a/src/main/js/sortable-drag-drop.js +++ b/src/main/js/sortable-drag-drop.js @@ -17,7 +17,30 @@ function registerSortableDragDrop(e) { return false; } + let initialX, currentItem; + const maxRotation = 2; // Maximum rotation in degrees + const maxDistance = 150; // Maximum distance for the full rotation effect + + function onPointerMove(evt) { + if (!currentItem) { + return; + } + + const currentX = evt.clientX + window.scrollX; + const distanceX = currentX - initialX - 20; + + // Calculate rotation angle based on the distance moved + const rotation = Math.max( + -maxRotation, + Math.min(maxRotation, (distanceX / maxDistance) * maxRotation), + ); + + currentItem.style.rotate = `${rotation}deg`; + currentItem.style.translate = distanceX * -0.75 + "px"; + } + new Sortable(e, { + animation: 200, draggable: ".repeated-chunk", handle: ".dd-handle", ghostClass: "repeated-chunk--sortable-ghost", @@ -25,13 +48,18 @@ function registerSortableDragDrop(e) { forceFallback: true, // Do not use html5 drag & drop behaviour because it does not work with autoscroll scroll: true, bubbleScroll: true, - onChoose: function (event) { - const draggableDiv = event.item; - const height = draggableDiv.clientHeight; - draggableDiv.style.height = `${height}px`; + onStart: function (evt) { + const rect = evt.item.getBoundingClientRect(); + initialX = rect.left + window.scrollX; + currentItem = document.querySelector(".sortable-drag"); + document.addEventListener("pointermove", onPointerMove); }, - onUnchoose: function (event) { - event.item.style.removeProperty("height"); + onEnd: function () { + document.removeEventListener("pointermove", onPointerMove); + if (currentItem) { + currentItem.style.rotate = ""; + currentItem = null; + } }, }); } diff --git a/src/main/scss/abstracts/_mixins.scss b/src/main/scss/abstracts/_mixins.scss index 7999284e1a96..fcdf10ed357a 100644 --- a/src/main/scss/abstracts/_mixins.scss +++ b/src/main/scss/abstracts/_mixins.scss @@ -61,6 +61,10 @@ z-index: 0; text-decoration: none !important; border-radius: 0.66rem; + cursor: pointer; + border: none; + outline: none; + background: transparent; &::before, &::after { @@ -74,7 +78,7 @@ } &::before { - background-color: transparent; + background-color: var(--item-background); } &::after { diff --git a/src/main/scss/form/_reorderable-list.scss b/src/main/scss/form/_reorderable-list.scss index 7cd726556df1..d1b536f19e2a 100644 --- a/src/main/scss/form/_reorderable-list.scss +++ b/src/main/scss/form/_reorderable-list.scss @@ -1,22 +1,229 @@ -/* ========================= repeatable elements ========================= */ +@use "../abstracts/mixins"; + +$inset: 1rem; +$exit-animation: 0.2s; .repeated-chunk { position: relative; - border: 2px dashed var(--input-border); - padding: 1rem; - border-radius: 10px; - margin-bottom: 1rem; - margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + background: var(--card-background); + border-radius: calc(var(--form-input-border-radius) / 2); transition: - opacity 0.2s ease-in, - max-height 0.2s ease-in; + max-height #{$exit-animation} ease, + border-radius var(--standard-transition), + box-shadow var(--standard-transition); + height: unset !important; + margin: 0 0 var(--card-border-width) 0; + + &::after { + content: ""; + position: absolute; + inset: 0; + border: var(--card-border-width) solid var(--card-border-color); + border-radius: inherit; + pointer-events: none; + } + + &:first-of-type { + border-top-left-radius: var(--form-input-border-radius); + border-top-right-radius: var(--form-input-border-radius); + } + + &:last-of-type { + border-bottom-left-radius: var(--form-input-border-radius); + border-bottom-right-radius: var(--form-input-border-radius); + } + + &__header { + position: relative; + display: flex; + align-items: center; + justify-content: flex-start; + font-weight: var(--form-label-font-weight); + min-height: 46px; + + .dd-handle { + position: relative; + width: 44px; + height: 46px; + cursor: move; + opacity: 0.75; + color: var(--text-color-secondary); + transition: var(--standard-transition); + margin-right: -2px; + + &::after { + content: ""; + position: absolute; + inset: 0; + background-color: currentColor; + mask-image: url("data:image/svg+xml,%3Csvg width='8' height='13' viewBox='0 0 8 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_0_12)'%3E%3Ccircle cx='1.5' cy='1.5' r='1' stroke='black'/%3E%3Ccircle cx='6.5' cy='1.5' r='1' stroke='black'/%3E%3Ccircle cx='1.5' cy='6.5' r='1' stroke='black'/%3E%3Ccircle cx='6.5' cy='6.5' r='1' stroke='black'/%3E%3Ccircle cx='1.5' cy='11.5' r='1' stroke='black'/%3E%3Ccircle cx='6.5' cy='11.5' r='1' stroke='black'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_0_12'%3E%3Crect width='8' height='13' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A"); + mask-position: center; + mask-repeat: no-repeat; + } + + &:hover { + opacity: 1; + color: var(--text-color); + } + } + + &--no-handle { + padding-left: $inset; + } + } + + .repeatable-delete { + @include mixins.item; + + --item-background: color-mix(in sRGB, currentColor 7.5%, transparent); + --item-background--hover: color-mix(in sRGB, currentColor 15%, transparent); + --item-background--active: color-mix( + in sRGB, + currentColor 25%, + transparent + ); + --item-box-shadow--focus: color-mix( + in sRGB, + currentColor 7.5%, + transparent + ); + + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: grid; + place-items: center; + place-content: center; + width: 30px; + height: 30px; + margin-left: auto; + color: var(--red); + padding: 0; + transition: var(--standard-transition); + font-size: 0.65rem; + + &::before, + &::after { + inset: 4px; + border-radius: 0.5rem; + } + + &::before { + border: 1px solid color-mix(in sRGB, currentColor 2.5%, transparent); + } + + // Center and overlap SVG and text content + * { + grid-area: container; + } + + svg { + width: 0.875rem; + height: 0.875rem; + transition: var(--standard-transition); + + * { + stroke-width: 40px; + } + } + + span { + opacity: 0; + transition: var(--standard-transition); + filter: blur(2px); + scale: 0.5; + text-wrap: nowrap; + } + + &:hover, + &:active, + &:focus { + // This value is set in Jelly - see repeatableDeleteButton.jelly + width: var(--width); + + &::before, + &::after { + border-radius: 1rem; + } + + svg { + opacity: 0; + filter: blur(2px); + scale: 0.6; + rotate: -90deg; + } + + span { + opacity: 1; + filter: none; + scale: 1; + } + } + } +} + +@keyframes repeated-chunk-entrance-animation { + from { + scale: 0.99; + opacity: 0; + filter: blur(1px); + } +} + +@keyframes repeated-chunk-exit-animation { + to { + scale: 0.98; + opacity: 0; + filter: blur(3px); + translate: 0 -10%; + } +} + +.repeated-chunk.fade-in { + animation: repeated-chunk-entrance-animation 0.3s both; } -.repeated-chunk.fade-in, .repeated-chunk.fade-out { - opacity: 0; + z-index: -10; + animation: repeated-chunk-exit-animation #{$exit-animation} both; } +.repeated-chunk--sortable-chosen { + width: 100%; + height: 100px; + box-shadow: 0 15px 30px rgb(0 0 20 / 0.05); + opacity: 1 !important; + backdrop-filter: blur(20px); + border-radius: var(--form-input-border-radius) !important; + z-index: 10; +} + +.repeated-chunk--sortable-ghost { + opacity: 0 !important; +} + +.jenkins-repeated-chunk__content { + padding: 0 $inset $inset; + + &:empty { + display: none; + } + + & > *:last-of-type { + margin-bottom: 0; + } +} + +.repeated-chunk + .repeatable-insertion-point + .hetero-list-add { + margin-top: 1rem; + transition: none; +} + +/* ========================= repeatable elements ========================= */ + .repeated-chunk .show-if-last { visibility: hidden; } @@ -99,143 +306,3 @@ div.to-be-removed { display: none; } - -/* ========================= D&D support in heterogenous/repeatable lists = */ - -.hetero-list-container.with-drag-drop .repeated-chunk, -.repeated-container.with-drag-drop .repeated-chunk { - margin-bottom: 1rem; -} - -// SortableJS drag & drop classes -.repeated-chunk--sortable-ghost { - height: 100px; - width: 100%; - overflow: hidden; -} - -.repeated-chunk--sortable-chosen { - width: 100%; - height: 100px; - background-color: transparent; - border: 2px solid var(--accent-color); - - & > * { - display: none; - } -} - -.repeated-chunk { - & > div > *:last-of-type { - margin-bottom: 0; - } - - &__header { - display: flex; - align-items: center; - justify-content: flex-start; - font-weight: bold; - margin-top: -0.4rem; - margin-bottom: 0.75rem; - - .dd-handle { - position: relative; - width: 30px; - height: 30px; - overflow: hidden; - margin-right: 0.75rem; - cursor: move; - margin-left: -6px; - - &::before { - content: ""; - position: absolute; - inset: 0; - background: var(--text-color); - border-radius: var(--form-input-border-radius); - opacity: 0; - transition: var(--standard-transition); - } - - &::after { - content: ""; - position: absolute; - inset: 0 4px; - background-color: var(--text-color); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'%3E%3Ctitle%3EReorder Three%3C/title%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M96 256h320M96 176h320M96 336h320'/%3E%3C/svg%3E"); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - &:hover { - &::before { - opacity: 0.1; - } - } - } - } - - // TODO: Update/remove when .jenkins-button PR is merged - .repeatable-delete { - position: absolute; - top: 0.6rem; - right: 0.6rem; - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: none; - outline: none; - margin-left: auto; - color: var(--red); - z-index: 0; - background: transparent; - cursor: pointer; - - &::before { - content: ""; - position: absolute; - inset: 0; - background: currentColor; - border-radius: 100px; - z-index: -1; - opacity: 0.075; - transition: var(--standard-transition); - } - - &::after { - content: ""; - position: absolute; - inset: 0; - box-shadow: 0 0 0 10px transparent; - border-radius: 100px; - z-index: -1; - opacity: 0.075; - transition: var(--standard-transition); - } - - svg { - width: 18px; - height: 18px; - } - - &:hover { - &::before { - opacity: 0.1; - } - } - - &:active, - &:focus { - &::before { - opacity: 0.15; - } - - &::after { - box-shadow: 0 0 0 5px var(--red); - } - } - } -}