Explorar o código

Improve subscription list drag and drop (#3953)

* it works

* more beautiful

* optimize JS

* CSS - optimized for dark theme

* delete not used form

* phpcs

* optimize

* more optimization

* fixed javaScript syntax

* better css class name

* template.css RTL

* fix failed test for RTL

* fix broken CSS Syntax

* fixed failed test

* fixed: empty lines in RTL CSS

* again a fixed CSS test....

* fixed test

* less magic numbers
maTh %!s(int64=4) %!d(string=hai) anos
pai
achega
9224668285

+ 39 - 42
app/views/subscription/index.phtml

@@ -3,7 +3,7 @@
 	$this->partial('aside_subscription');
 ?>
 
-<main class="post drop-section">
+<main class="post">
 	<div class="link-back-wrapper">
 		<a class="link-back" href="<?= _url('index', 'index') ?>"><?= _t('gen.action.back_to_rss_feeds') ?></a>
 	</div>
@@ -25,55 +25,53 @@
 			<a class="btn" href="<?= _url('subscription', 'index', 'error', '1') ?>"><?= _i('look') ?> <?= _t('sub.feed.show.error') ?></a>
 		</div>
 	<?php } ?>
-
-	<form id="controller-category" method="post" aria-hidden="true">
-		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
-	</form>
-
-	<?php
-		$signalError = false;
-		foreach ($this->categories as $cat) {
-			$feeds = $cat->feeds();
-	?>
-	<div class="box">
-		<div class="box-title">
-			<a class="configure open-slider" href="<?= _url('subscription', 'category', 'id', $cat->id()) ?>"><?= _i('configure') ?></a>
-			<?= $cat->name() ?>
-		</div>
-		<ul class="box-content" data-cat-id="<?= $cat->id() ?>">
-			<?php if (!empty($feeds)) { ?>
-			<?php
+	
+	<div class="drop-section">
+		<?php
+			$signalError = false;
+			foreach ($this->categories as $cat) {
+				$feeds = $cat->feeds();
+		?>
+		<div class="box">
+			<div class="box-title">
+				<a class="configure open-slider" href="<?= _url('subscription', 'category', 'id', $cat->id()) ?>"><?= _i('configure') ?></a>
+				<?= $cat->name() ?>
+			</div>
+			<ul class="box-content drop-zone" dropzone="move" data-cat-id="<?= $cat->id() ?>">
+				<?php
+				if (!empty($feeds)) {
 					foreach ($feeds as $feed) {
 						if ($this->onlyFeedsWithError && !$feed->inError()) {
 							continue;
 						}
 						$error = $feed->inError() ? ' error' : '';
 						$empty = $feed->nbEntries() == 0 ? ' empty' : '';
-			?>
-			<li class="item feed<?= $error, $empty, $feed->mute() ? ' mute' : '' ?>"
-				draggable="true"
-				data-feed-id="<?= $feed->id() ?>"
-				dropzone="move">
-				<a class="configure open-slider" href="<?= _url('subscription', 'feed', 'id', $feed->id()) ?>"><?= _i('configure') ?></a>
-				<?php if (FreshRSS_Context::$user_conf->show_favicons): ?><img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php endif; ?>
-				<?= $feed->name() ?>
-			</li>
-			<?php 	}
+				?>
+				<li class="item feed<?= $error, $empty, $feed->mute() ? ' mute' : '' ?>"
+					draggable="true"
+					data-feed-id="<?= $feed->id() ?>">
+					<a class="configure open-slider" href="<?= _url('subscription', 'feed', 'id', $feed->id()) ?>"><?= _i('configure') ?></a>
+					<?php if (FreshRSS_Context::$user_conf->show_favicons): ?><img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php endif; ?>
+					<?= $feed->name() ?>
+				</li>
+				<?php
+					}
 				} else {
-			?>
-			<li class="item feed disabled" dropzone="move"><div class="alert-warn"><?= _t('sub.category.empty') ?></div></li>
-			<?php } ?>
-			<li class="item feed">✚ <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.feed.add') ?></a></li>
-		</ul>
-	</div>
-	<?php } ?>
+				?>
+				<li class="item feed disabled"><div class="alert-warn"><?= _t('sub.category.empty') ?></div></li>
+				<?php } ?>
+				<li class="item feed">✚ <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.feed.add') ?></a></li>
+			</ul>
+		</div>
+		<?php } ?>
 
-	<div class="box visible-semi">
-		<div class="box-title">
-			✚ <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.category.add') ?></a>
+		<div class="box visible-semi">
+			<div class="box-title">
+				✚ <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.category.add') ?></a>
+			</div>
+			<div class="box-content">
+			</div>
 		</div>
-		<ul class="box-content">
-		</ul>
 	</div>
 
 	<?php $class = $this->displaySlider ? ' class="active"' : ''; ?>
@@ -89,5 +87,4 @@
 		}
 	?>
 	</div>
-
 </main>

+ 41 - 24
p/scripts/category.js

@@ -25,7 +25,7 @@ function dragend_process(t) {
 
 		if (p.childElementCount <= 1) {
 			p.insertAdjacentHTML('afterbegin',
-				'<li class="item feed disabled" dropzone="move"><div class="alert-warn">' + context.i18n.category_empty + '</div></li>');
+				'<li class="item feed disabled"><div class="alert-warn">' + context.i18n.category_empty + '</div></li>');
 		}
 	}
 }
@@ -47,41 +47,53 @@ function init_draggable() {
 	const dropSection = document.querySelector('.drop-section');
 
 	dropSection.ondragstart = function (ev) {
-		const li = ev.target.closest ? ev.target.closest(draggable) : null;
-		if (li) {
+		const li_draggable = ev.target.closest ? ev.target.closest(draggable) : null;
+		if (li_draggable) {
+			const ulClosest = li_draggable.closest('ul');
+			ulClosest.classList.add('drag-disallowed');
+			ulClosest.removeAttribute('dropzone', '');
 			const drag = ev.target.closest('[draggable]');
 			ev.dataTransfer.effectAllowed = 'move';
 			dragHtml = drag.outerHTML;
 			dragFeedId = drag.getAttribute('data-feed-id');
 			ev.dataTransfer.setData('text', dragFeedId);
-			drag.style.opacity = 0.3;
+			drag.style.opacity = 0.5;
+			drag.classList.add('dragging');
+			li_draggable.closest('.drop-section').classList.add('drag-active');
 			dnd_successful = false;
 		}
 	};
 
 	dropSection.ondragend = function (ev) {
-		const li = ev.target.closest ? ev.target.closest(draggable) : null;
-		if (li) {
-			dragend_process(li);
+		const li_draggable = ev.target.closest ? ev.target.closest(draggable) : null;
+		if (li_draggable) {
+			dragend_process(li_draggable);
+		}
+		li_draggable.classList.remove('dragging');
+		const disallowDragging = document.getElementsByClassName('drag-disallowed');
+		for (let i = 0; i < disallowDragging.length; i++) {
+			disallowDragging[i].setAttribute('dropzone', 'move');
+			disallowDragging[i].classList.remove('drag-disallowed');
 		}
+		li_draggable.closest('.drag-active').classList.remove('drag-active');
 	};
 
 	dropSection.ondragenter = function (ev) {
-		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-		if (li) {
-			li.classList.add('drag-hover');
+		const ul_dropzone = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (ul_dropzone) {
+			ul_dropzone.classList.add('drag-hover');
 			return false;
 		}
 	};
 
 	dropSection.ondragleave = function (ev) {
-		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-		if (li) {
+		const ul_dropzone = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (ul_dropzone) {
 			const scroll_top = document.documentElement.scrollTop;
-			const top = li.offsetTop;
-			const left = li.offsetLeft;
-			const right = left + li.clientWidth;
-			const bottom = top + li.clientHeight;
+			const top = ul_dropzone.offsetTop;
+			const left = ul_dropzone.offsetLeft;
+			const right = left + ul_dropzone.clientWidth;
+			const bottom = top + ul_dropzone.clientHeight;
 			const mouse_x = ev.screenX;
 			const mouse_y = ev.clientY + scroll_top;
 
@@ -90,21 +102,22 @@ function init_draggable() {
 				// HACK because dragleave is triggered when hovering children!
 				return;
 			}
-			li.classList.remove('drag-hover');
+			ul_dropzone.classList.remove('drag-hover');
 		}
 	};
 
 	dropSection.ondragover = function (ev) {
 		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
 		if (li) {
+			li.closest('ul').classList.remove('drag-drop');
 			ev.dataTransfer.dropEffect = 'move';
 			return false;
 		}
 	};
 
 	dropSection.ondrop = function (ev) {
-		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-		if (li) {
+		const ul_dropzone = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (ul_dropzone) {
 			loading = true;
 
 			const req = new XMLHttpRequest();
@@ -112,9 +125,12 @@ function init_draggable() {
 			req.responseType = 'json';
 			req.onload = function (e) {
 				if (this.status == 200) {
-					li.insertAdjacentHTML('afterend', dragHtml);
-					if (li.classList.contains('disabled')) {
-						li.remove();
+					ul_dropzone.insertAdjacentHTML('afterbegin', dragHtml);
+					ul_dropzone.firstChild.classList.add('moved');
+					ul_dropzone.scrollTop = 0;
+					const disabledElement = ul_dropzone.getElementsByClassName('disabled');
+					if (disabledElement.length > 0) {
+						disabledElement[0].remove();
 					}
 					dnd_successful = true;
 				}
@@ -127,11 +143,12 @@ function init_draggable() {
 			req.setRequestHeader('Content-Type', 'application/json');
 			req.send(JSON.stringify({
 				f_id: dragFeedId,
-				c_id: li.parentElement.getAttribute('data-cat-id'),
+				c_id: ul_dropzone.getAttribute('data-cat-id'),
 				_csrf: context.csrf,
 			}));
 
-			li.classList.remove('drag-hover');
+			ul_dropzone.closest('ul').classList.add('drag-drop');
+			ul_dropzone.closest('ul').classList.remove('drag-hover');
 			return false;
 		}
 	};

+ 51 - 3
p/themes/base-theme/template.css

@@ -516,6 +516,14 @@ a.btn {
 	display: block;
 }
 
+.box .box-content .item.feed.moved {
+	font-style: italic;
+}
+
+.box .box-content .item.feed.moved .favicon {
+	opacity: 0.4;
+}
+
 .box .box-content .item.disabled {
 	text-align: center;
 	font-style: italic;
@@ -531,13 +539,53 @@ a.btn {
 }
 
 /*=== Draggable */
-.drag-hover {
+[draggable=true]:hover {
+	cursor: move;
+}
+
+.dragging {
+	background-color: rgba(255, 255, 0, 0.7);
+}
+
+.dragging .icon {
+	visibility: hidden;
+}
+
+.drag-disallowed {
+	opacity: 0.5;
+}
+
+.drag-active .drop-zone:not(.drag-disallowed) {
+	background: repeating-linear-gradient(45deg, transparent, transparent 40px, rgba(250,250,210, 0.7) 40px, rgba(250,250,210, 0.7) 60px);
+}
+
+.drag-active .drag-hover.drop-zone {
+	background: rgba(250,250,210, 0.7);
+	transition: background 0.5s;
+}
+
+li.drag-hover {
 	margin: 0 0 5px;
 	border-bottom: 2px solid #ccc;
 }
 
-[draggable=true] {
-	cursor: grab;
+.drag-drop {
+	animation-name: droppedKeyframe;
+	animation-duration: 0.7s;
+}
+
+@keyframes droppedKeyframe {
+	0% {
+		background-color: rgba(250,250,210, 0.7);
+	}
+
+	50% {
+		background-color: yellow;
+	}
+
+	100% {
+		background-color: none;
+	}
 }
 
 /*=== Scrollbar */

+ 51 - 3
p/themes/base-theme/template.rtl.css

@@ -516,6 +516,14 @@ a.btn {
 	display: block;
 }
 
+.box .box-content .item.feed.moved {
+	font-style: italic;
+}
+
+.box .box-content .item.feed.moved .favicon {
+	opacity: 0.4;
+}
+
 .box .box-content .item.disabled {
 	text-align: center;
 	font-style: italic;
@@ -531,13 +539,53 @@ a.btn {
 }
 
 /*=== Draggable */
-.drag-hover {
+[draggable=true]:hover {
+	cursor: move;
+}
+
+.dragging {
+	background-color: rgba(255, 255, 0, 0.7);
+}
+
+.dragging .icon {
+	visibility: hidden;
+}
+
+.drag-disallowed {
+	opacity: 0.5;
+}
+
+.drag-active .drop-zone:not(.drag-disallowed) {
+	background: repeating-linear-gradient(-45deg, transparent, transparent 40px, rgba(250,250,210, 0.7) 40px, rgba(250,250,210, 0.7) 60px);
+}
+
+.drag-active .drag-hover.drop-zone {
+	background: rgba(250,250,210, 0.7);
+	transition: background 0.5s;
+}
+
+li.drag-hover {
 	margin: 0 0 5px;
 	border-bottom: 2px solid #ccc;
 }
 
-[draggable=true] {
-	cursor: grab;
+.drag-drop {
+	animation-name: droppedKeyframe;
+	animation-duration: 0.7s;
+}
+
+@keyframes droppedKeyframe {
+	0% {
+		background-color: rgba(250,250,210, 0.7);
+	}
+
+	50% {
+		background-color: yellow;
+	}
+
+	100% {
+		background-color: none;
+	}
 }
 
 /*=== Scrollbar */