From efe1610101ba3a61afc9efdf09229f0c6a8bafda Mon Sep 17 00:00:00 2001
From: nicod_ <nicod@lerebooteux.fr>
Date: Sun, 16 Apr 2023 18:30:41 +0200
Subject: [PATCH] Gestion des rangs des blocs sur un objet
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Code repris en grande partie du plugin medias (markup html, javascript et action)
TODO : mutualiser ce code relativement générique au niveau du core de SPIP ?
---
 TODO.md                                   |   2 +-
 action/ordonner_liens_blocs.php           | 104 +++++++++++++++++++++
 javascript/gestion_listes_blocs.js.html   | 107 ++++++++++++++++++++++
 prive/squelettes/inclure/blocs_objet.html |  37 ++++++--
 prive/style_prive_plugin_blocks.html      |  41 +++++++++
 5 files changed, 281 insertions(+), 10 deletions(-)
 create mode 100644 action/ordonner_liens_blocs.php
 create mode 100644 javascript/gestion_listes_blocs.js.html
 create mode 100644 prive/style_prive_plugin_blocks.html

diff --git a/TODO.md b/TODO.md
index a27d974..34a1dc6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -8,7 +8,7 @@
 
 [x] Ajouter des blocs sous les objets liés
 
-[ ] Gérer les rangs des blocs sous les objets liés
+[x] Gérer les rangs des blocs sous les objets liés
 
 [ ] Afficher les blocs sous les objets liés : largeur limitée, affichage simplifié + preview en popin ?
 
diff --git a/action/ordonner_liens_blocs.php b/action/ordonner_liens_blocs.php
new file mode 100644
index 0000000..10cf385
--- /dev/null
+++ b/action/ordonner_liens_blocs.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * Action ordonnant un lien sur une table de liens
+ *
+ * @plugin     Medias
+ * @copyright  2017
+ * @author     Matthieu Marcillaud
+ * @licence    GNU/GPL
+ * @package    SPIP\Ordoc\Action
+ */
+
+if (!defined('_ECRIRE_INC_VERSION')) {
+	return;
+}
+
+function action_ordonner_liens_blocs_dist() {
+	action_ordonner_liens_dist();
+}
+
+function action_ordonner_liens_dist() {
+	include_spip('inc/autoriser');
+	include_spip('base/objets');
+	include_spip('action/editer_liens');
+
+	// source (table spip_xx_liens)
+	$objet = objet_type(_request('objet_source'));
+
+	// objet lié
+	$objet_lie = objet_type(_request('objet_lie'));
+	$id_objet_lie = intval(_request('id_objet_lie'));
+
+	// ordre des éléments
+	$ordre = _request('ordre');
+
+	if (!$objet or !$objet_lie or !$id_objet_lie or !$ordre or !is_array($ordre) or !objet_associable($objet)) {
+		return envoyer_json_erreur(_T('medias:erreur_objet_absent') . ' ' . _T('medias:erreur_deplacement_impossible'));
+	}
+
+	if (!autoriser('modifier', $objet_lie, $id_objet_lie)) {
+		return envoyer_json_erreur(_T('medias:erreur_autorisation') . ' ' . _T('medias:erreur_deplacement_impossible'));
+	}
+
+	[$_id_objet, $table_liens] = objet_associable($objet);
+
+	$success = $errors = [];
+
+	$actuels = sql_allfetsel(
+		[$_id_objet . ' AS id', 'rang_lien'],
+		$table_liens,
+		[
+			sql_in($_id_objet, $ordre),
+			'objet = ' . sql_quote($objet_lie),
+			'id_objet = ' . sql_quote($id_objet_lie),
+		]
+	);
+
+	$futurs = array_flip($ordre);
+	// ordre de 1 à n (pas de 0 à n).
+	array_walk($futurs, function (&$v) {
+		$v++;
+	});
+
+	$updates = [];
+
+	foreach ($actuels as $l) {
+		if ($futurs[$l['id']] !== $l['rang_lien']) {
+			$updates[$l['id']] = $futurs[$l['id']];
+		}
+	}
+
+	if ($updates) {
+		foreach ($updates as $id => $ordre) {
+			sql_updateq(
+				$table_liens,
+				['rang_lien' => $ordre],
+				[
+					$_id_objet . ' = ' . $id,
+					'objet = ' . sql_quote($objet_lie),
+					'id_objet = ' . sql_quote($id_objet_lie),
+				]
+			);
+		}
+	}
+
+	return envoyer_json_envoi([
+		'done'    => true,
+		'success' => $success,
+		'errors'  => $errors,
+	]);
+}
+
+function envoyer_json_envoi($data) {
+	header('Content-Type: application/json; charset=' . $GLOBALS['meta']['charset']);
+	echo json_encode($data, JSON_THROW_ON_ERROR);
+}
+
+function envoyer_json_erreur($msg) {
+	return envoyer_json_envoi([
+		'done'    => false,
+		'success' => [],
+		'errors'  => [$msg],
+	]);
+}
diff --git a/javascript/gestion_listes_blocs.js.html b/javascript/gestion_listes_blocs.js.html
new file mode 100644
index 0000000..c5acba2
--- /dev/null
+++ b/javascript/gestion_listes_blocs.js.html
@@ -0,0 +1,107 @@
+#HTTP_HEADER{Content-Type: text/javascript; charset=#CHARSET}
+[(#REM)<script>/*
+
+	Gestion des listes de blocs :
+	- Gestion du tri par glisser-déposer
+
+	Markup :
+	- Listes              : .liste_items.blocs
+	- Listes ordonnables  : .liste_items.blocs.ordonner_rang_lien\[data-lien\]
+
+*/]
+
+/* Gestion du tri des listes de blocs et de leur enregistrement */
+function ordonner_listes_blocs() {
+
+	if (typeof Sortable === 'function') {
+		$(".liste_items.blocs.ordonner_rang_lien[data-lien]").find('> .sortable').each(function () {
+			// détruire / recréer le sortable à chaque appel ajax
+			if (Sortable.get(this)) {
+				Sortable.get(this).destroy();
+			}
+			// pas de tri possible s'il n'y a qu'un seul élément.
+			if ($(this).find('> .item').length < 2) {
+				$(this).find('.deplacer-bloc').hide();
+				$(this).parent().find('.tout_desordonner').hide();
+				return true; // continue
+			} else {
+				$(this).find('.deplacer-bloc').show();
+			}
+			new Sortable(this, {
+				direction: 'vertical',
+				swapThreshold: .8,
+				ghostClass: "deplacer-bloc-placeholder",
+				onStart: function(event) {
+					$(event.item).addClass('bloc-en-mouvement');
+				},
+				onEnd: function(event) {
+					$(event.item).removeClass('bloc-en-mouvement');
+				},
+				onUpdate: function (event) {
+					const ordre = this.toArray();
+					const $items = $(event.from);
+					const $item = $(event.item);
+
+					// l'objet lié est indiqué dans l'attribut data-lien sur la liste
+					const [objet_lie, id_objet_lie] = $items.parents(".liste_items.blocs").data("lien").split("/");
+					const action = '[(#VAL{ordonner_liens_blocs}|generer_url_action{"", 1})]';
+					const params = {
+						objet_source: 'bloc',
+						objet_lie: objet_lie,
+						id_objet_lie: id_objet_lie,
+						ordre: ordre,
+					};
+
+					$item.animateLoading();
+
+					$.post({
+						url: action,
+						data: params,
+						dataType: 'json',
+						cache: false,
+					}).done(function(data) {
+
+						const couleur_origine = $item.css('background-color');
+						const couleur_erreur = $("<div class='remove'></div>").css('background-color');
+						const couleur_succes = $("<div class='append'></div>").css('background-color');
+						$item.endLoading(true);
+
+						if (data.errors.length) {
+							$item.css({backgroundColor: couleur_erreur}).animate({backgroundColor: couleur_origine}, 'normal', () => {
+								$item.css({backgroundColor: ''});
+							});
+						} else {
+							$item.css({backgroundColor: couleur_succes}).animate({backgroundColor: couleur_origine}, 'normal', () => {
+								$item.css({backgroundColor: ''});
+							});
+							$items.parent().find('.tout_desordonner').show();
+						}
+					});
+				}
+			});
+
+			// bouton "désordonner"
+			if ($(this).parent().find('.deplacer-bloc[data-rang!=0]').length) {
+				$(this).parent().find('.tout_desordonner').show();
+			} else {
+				$(this).parent().find('.tout_desordonner').hide();
+			}
+		});
+	}
+}
+
+
+/* Initialisation et relance en cas de chargement ajax */
+if (window.jQuery) {
+	jQuery(function($){
+		if (!$.js_portfolio_blocs_charge) {
+			$.js_portfolio_blocs_charge = true;
+			if (typeof Sortable === "undefined") {
+				jQuery.getScript('[(#CHEMIN{prive/javascript/Sortable.js}|timestamp)]').done(ordonner_listes_blocs);
+			} else {
+				ordonner_listes_blocs();
+			}
+			onAjaxLoad(ordonner_listes_blocs);
+		}
+	});
+}
diff --git a/prive/squelettes/inclure/blocs_objet.html b/prive/squelettes/inclure/blocs_objet.html
index ff9d194..37c5ce9 100644
--- a/prive/squelettes/inclure/blocs_objet.html
+++ b/prive/squelettes/inclure/blocs_objet.html
@@ -1,20 +1,39 @@
 <B>
 [<h2>(#GRAND_TOTAL|singulier_ou_pluriel{bloc:info_1_bloc,bloc:info_nb_blocs})</h2>]
-<BOUCLE(BLOCS) {objet}{id_objet} {statut?} {par rang_lien}>
-	<div id="bloc#ID_BLOC">
-	#BOITE_OUVRIR{#INFO_TITRE{blocs_types,#ID_BLOCS_TYPE}}
 
-	#GENERER_BLOCK
+<div class="liste_items blocs ordonner_rang_lien" data-lien="#ENV{objet}/#ENV{id_objet}">
+	<div class="sortable">
+	<BOUCLE(BLOCS) {objet}{id_objet} {statut?} {par rang_lien}>
+		<div class="item bloc statut_#STATUT" id="bloc#ID_BLOC" data-id="#ID_BLOC">
+			<h3 class="titrem bloc__type">
+				[(#CHEMIN_IMAGE{bloc-16.png}|balise_img)]#INFO_TITRE{blocs_types,#ID_BLOCS_TYPE}
+				[(#AUTORISER{modifier,bloc,#ID_BLOC})
+				<span class="deplacer-bloc" data-rang="#RANG_LIEN">
+					<img src='#CHEMIN_IMAGE{deplacer-16.png}' width='16' height='16' alt='<:medias:ordonner_ce_document|attribut_html:>' title='<:medias:ordonner_ce_document|attribut_html:>' />
+				</span>
+				]
+			</h3>
 
-	[(#AUTORISER{modifier,bloc,#ID_BLOC})
-		[(#URL_ECRIRE{#VAL{bloc}|objet_info{url_edit},id_bloc=#ID_BLOC}|parametre_url{redirect,#SELF}|concat{'#bloc',#ID_BLOC}|icone_verticale{<:bloc:icone_modifier_bloc:/>,bloc,edit,right})]
-	]
+			#GENERER_BLOCK
 
-	#BOITE_FERMER
+			[(#AUTORISER{modifier,bloc,#ID_BLOC})
+			<div class="deplacer-modifier">
+				[(#URL_ECRIRE{#VAL{bloc}|objet_info{url_edit},id_bloc=#ID_BLOC}|parametre_url{redirect,#SELF}|concat{'#bloc',#ID_BLOC}|icone_verticale{<:bloc:icone_modifier_bloc:/>,bloc,edit,right})]
+			</div>
+			]
+
+		</div>
+	</BOUCLE>
 	</div>
-</BOUCLE>
+</div>
+
 </B>
 
 <div class="ajax">
 #FORMULAIRE_EDITER_BLOC{new,#OBJET,#ID_OBJET,#GET{redirect}}
 </div>
+
+<script type="text/javascript">
+/* Gestion du tri des blocs */
+[(#INCLURE{fond=javascript/gestion_listes_blocs.js}|compacte{js})]
+</script>
diff --git a/prive/style_prive_plugin_blocks.html b/prive/style_prive_plugin_blocks.html
new file mode 100644
index 0000000..8d42976
--- /dev/null
+++ b/prive/style_prive_plugin_blocks.html
@@ -0,0 +1,41 @@
+[(#REM)
+
+	Ce squelette definit les styles de l'espace prive
+
+	Note: l'entete "Vary:" sert a repousser l'entete par
+	defaut "Vary: Cookie,Accept-Encoding", qui est (un peu)
+	genant en cas de "rotation du cookie de session" apres
+	un changement d'IP (effet de clignotement).
+	<style>
+]
+#CACHE{3600*100,cache-client}
+#HTTP_HEADER{Content-Type: text/css; charset=utf-8}
+#HTTP_HEADER{Vary: Accept-Encoding}
+
+#SET{claire,##ENV{couleur_claire,edf3fe}}
+#SET{foncee,##ENV{couleur_foncee,3874b0}}
+#SET{left,#ENV{ltr}|choixsiegal{left,left,right}}
+#SET{right,#ENV{ltr}|choixsiegal{left,right,left}}
+
+.liste_items.blocs {
+
+}
+
+.liste_items.blocs .item.bloc {
+	padding: calc(var(--spip-list-spacing-y) / 2) calc(var(--spip-list-spacing-x) / 2);
+	border:1px solid var(--spip-color-gray-light);
+	border-radius:5px;
+	margin-bottom: 1rem;
+}
+
+.liste_items.blocs .item.bloc .bloc__type {
+	margin-bottom: 1rem;
+}
+
+.deplacer-bloc {
+	display: inline-flex;
+	float: var(--spip-right);
+	margin-top: 1px;
+	cursor: move;
+}
+.item.deplacer-bloc-placeholder { background-color: var(--spip-color-theme-lighter); }
-- 
GitLab