'use strict';

/**
 * Gestion des boîtes flottantes.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
function Boxes()
{
	// Raccourcis.
	const _q = App.q, _qAll = App.qAll;



	// Durée d'animation pour l'ouverture et la fermeture des boîtes.
	var _animateDuration = 250;

	// Propriétés pour chaque boîte.
	var _box, _boxAjax, _boxName, _boxPage, _boxReportTimeout;

	// Boutton qui a ouvert une boîte.
	var _button;

	// Propriétés de la boîte d'édition des commentaires.
	var _comment, _commentId, _commentGuest;



	/**
	 * Ouverture d'une boîte.
	 *
	 * @param object evt
	 *
	 * @return void
	 */
	this.open = function(evt)
	{
		_boxName = 'box_' + App.attr(this, 'data-box');
		_box = _q('#' + _boxName);
		_button = this;

		// Création des boîtes.
		if (!_box)
		{
			switch (App.attr(this, 'data-box'))
			{
				case 'category' :
					_createBoxCategory();
					break;

				case 'comment' :
					_createBoxComment();
					break;

				case 'emoji' :
					_createBoxEmoji();
					break;

				case 'favorites_delete' :
					_createBoxFavoritesDelete();
					break;

				case 'item' :
					_createBoxItem();
					break;

				case 'login' :
					_createBoxLogin();
					break;

				case 'password' :
					_createBoxPassword();
					break;

				case 'selection' :
					_createBoxSelection();
					break;
			}
		}

		// Boîtes à ne pas afficher sur écrans de petite taille.
		if (App.attr(_box, 'data-box-large-screen') && Gallery.isSmallScreen())
		{
			return;
		}

		// Fermeture de la boîte.
		if (_box.checkVisibility())
		{
			_closeBox(_animateDuration, true);
		}

		// Ouverture de la boîte.
		else
		{
			/* Édition des commentaires. */
			if (_boxName == 'box_comment')
			{
				_openBoxComment();
			}

			// Fermeture de toute les boîtes.
			App.hide('.box_outer');

			// Ouverture de la boîte.
			App.show(_box, _animateDuration);

			// Page courante.
			_boxPage = [..._qAll(_box, '.box_page')].find(e => e.checkVisibility());

			// Gestion du focus.
			const page_focus = _q(_boxPage, '.focus');
			if (page_focus && !Gallery.isSmallScreen())
			{
				page_focus.focus();
			}
			else
			{
				_q(_box, `.box_menu a[data-box-page-id="${_boxPage.id}"]`).focus();
			}

			App.on(window, 'keyup', _keyUpBox);
		}

		evt.preventDefault();
	};



	/**
	 * Gestion des requêtes Ajax.
	 *
	 * @param object data
	 * @param function callback
	 *
	 * @return void
	 */
	function _ajax(data, callback)
	{
		if (_boxAjax)
		{
			return;
		}

		_loadingStart();

		_boxAjax = App.ajax(data,
		{
			always: r =>
			{
				_boxAjax = null;

				if (r === null)
				{
					_reportShow('error', 'Null error.');
					return;
				}

				switch (r?.status)
				{
					case 'error' :
					case 'info' :
					case 'success' :
						if (r.message)
						{
							_reportShow(r.status, r.message);
						}
						callback?.(r);
						break;

					default :
						_reportShow('error', 'Unknown error.');
						break;
				}
			}
		},
		r =>
		{
			_reportShow('error', 'PHP error.');
			console.log(_boxAjax.responseText);
		});
	}

	/**
	 * Fermeture d'une boîte.
	 *
	 * @param integer duration
	 * @param boolean setfocus
	 * @param function callback
	 *
	 * @return void
	 */
	function _closeBox(duration = 250, setfocus, callback)
	{
		_loadingStop();
		clearTimeout(_boxReportTimeout);

		App.hide(_box, duration, () =>
		{
			App.remove('#box_report');
			if (setfocus)
			{
				_button.focus();
			}
			callback?.();
		});

		App.off(window, 'keyup', _keyUpBox);
	};

	/**
	 * Création d'une boîte.
	 *
	 * @param object menu
	 * @param object pages
	 * @param boolean large_screen
	 *
	 * @return void
	 */
	function _createBox(menu, pages, large_screen = false)
	{
		// Structure de la boîte.
		_box = App.createElement(
			'<div class="box_outer">' +
				'<div class="box_inner">' +
					'<div class="box">' +
						'<ul class="box_menu"></ul>' +
					'</div>' +
				'</div>' +
			'</div>');
		App.attr(_box, 'id', _boxName);
		if (large_screen)
		{
			App.attr(_box, 'data-box-large-screen', 1);
		}

		// Menu.
		for (const data of menu)
		{
			if (data.condition === undefined || data.condition === true)
			{
				const li = App.createElement('<li><a href="javascript:;"><span></span></a></li>');
				const a = _q(li, 'a');
				if (data.id)
				{
					App.attr(a, 'id', data.id);
				}
				if (data.link)
				{
					App.attr(a, 'href', data.link);
				}
				if (data.page_id)
				{
					App.attr(a, 'data-box-page-id', data.page_id);
				}
				if (data.text)
				{
					App.appendText(a, data.text);
				}
				if (data.title)
				{
					App.attr(li, 'title', data.title);
				}
				App.html([li, 'span'], data.icon);
				App.append([_box, 'ul'], li);
			}
		}
		App.addClass([_box, '.box_menu li:first-child'], 'current');

		// Pages.
		for (const [name, data] of Object.entries(pages))
		{
			if (_q(_box, `[data-box-page-id="${name}"]`))
			{
				// Structure de la page.
				const page = App.createElement(
					'<div class="box_page">' +
						'<form>' +
							'<div class="box_content"></div>' +
							'<div class="box_buttons">' +
								'<input class="button" name="cancel" type="button">' +
							'</div>' +
						'</form>' +
					'</div>');
				App.attr(page, 'id', name);

				// Contenu de la page.
				App.append([page, '.box_content'], data.content);

				// Boutons
				if (data.buttons)
				{
					App.prepend([page, '.box_buttons'], data.buttons);
					App.val([page, '[name="delete"]'], BOX.l10n.delete);
					App.val([page, '[name="save"]'], BOX.l10n.save);
					App.val([page, '[name="submit"]'], BOX.l10n.submit);
				}
				App.val([page, '[name="cancel"]'], BOX.l10n.cancel);

				// Insertion de la page.
				App.append([_box, '.box'], page);
			}
		}

		// Page courante.
		_boxPage = _q(_box, '.box_page');

		// Changement de page.
		App.on([_box, '.box_menu a'], 'click', function(evt)
		{
			const id = App.attr(this, 'data-box-page-id');
			if (!id)
			{
				return;
			}
			evt.preventDefault();

			if (_boxPage.id != id)
			{
				if (_boxAjax)
				{
					return;
				}

				clearTimeout(_boxReportTimeout);
				App.remove('#box_report');
				_loadingStop();
			}

			// Nouvelle page courante.
			_boxPage = _q('#' + id);

			// Élément courant du menu.
			App.removeClass([_box, '.box_menu li.current'], 'current');
			App.addClass(this.closest('li'), 'current');

			// Changement de page.
			App.hide([this.closest('.box'), '.box_page']);
			App.show(_boxPage);
			if (!Gallery.isSmallScreen())
			{
				_q(_boxPage, '.focus')?.focus();
			}
		});

		// Bouton "Annuler".
		App.click([_box, '[name="cancel"]'], () => _closeBox(_animateDuration, true));

		// Insertion de la boîte.
		App.append('#content', _box);
	}

	/**
	 * Boîte d'édition d'une catégorie.
	 *
	 * @return void
	 */
	function _createBoxCategory()
	{
		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_category_infos',
				icon: '&#xe962;',
				text: BOX_EDIT.l10n.information
			}
		],
		{
			box_category_infos:
			{
				content:
					'<p class="field">' +
						'<label for="box_edit_category_title"></label>' +
						'<input id="box_edit_category_title"' +
							' required class="focus" maxlength="255" type="text">' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_edit_category_description"></label>' +
						'<textarea id="box_edit_category_description"' +
							' rows="10" cols="50" maxlength="65535"></textarea>' +
					'</p>',

				buttons:
					'<input class="button" name="save" type="submit">'
			}
		});

		// Texte localisé et données de formulaire.
		['title', 'description'].forEach(name =>
		{
			App.text(`[for="box_edit_category_${name}"]`, BOX_EDIT.l10n[name]);
			App.val('#box_edit_category_' + name, BOX_CATEGORY.form[name]);
		});

		// Enregistrement des informations.
		App.click('#box_category_infos input[name="save"]', () =>
		{
			if (App.val('#box_edit_category_title').trim() == '')
			{
				_q('#box_edit_category_title').focus();
				return;
			}
			_ajax(
			{
				section: 'cat-edit',
				cat_id: CATEGORY.id,
				title: App.val('#box_edit_category_title'),
				description: App.val('#box_edit_category_description')
			},
			r =>
			{
				// Titre.
				if (r.title !== undefined)
				{
					App.val('#box_edit_category_title', r.title);

					// Galerie.
					if (CATEGORY.id == 1)
					{
						App.text('title, h1 a', r.title);
					}

					// Catégorie.
					else
					{
						// Titre.
						App.text('#browse_inner .current a span, #breadcrumb .current', r.title);
						App.text('title', r.page_title);

						// URL.
						const url = new URL(window.location);
						window.history.pushState({}, '', Gallery.changeUrlname(url.href,
							'album|category', CATEGORY.id, r.urlname));
						App.attr('#breadcrumb a.current', 'href', Gallery.changeUrlname(
							_q('#breadcrumb a.current').href,
							'album|category', CATEGORY.id, r.urlname));
					}
				}

				// Description.
				if (r.description !== undefined)
				{
					App.val('#box_edit_category_description', r.description);
					if (GALLERY.page == 1)
					{
						App.remove('#cat_desc');
						if (r.description !== null)
						{
							App.before(
								_q('#pages_top')
									? '#pages_top'
									: _q('#page_thumbs') ? '#page_thumbs' : '#thumbs_cat',
								'<div id="cat_desc" class="object_desc">' +
									r.description_formated +
								'</div>'
							);
						}
					}
				}
			});
		});
	}

	/**
	 * Boîte d'édition d'un commentaire.
	 *
	 * @return void
	 */
	function _createBoxComment()
	{
		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_comment_edit',
				icon: '&#xe962;',
				text: BOX_COMMENT.l10n.edit
			},
			{
				page_id: 'box_comment_delete',
				icon: '&#xe9ac;',
				text: BOX.l10n.delete_tab
			}
		],
		{
			box_comment_edit:
			{
				content:
					'<p class="field">' +
						'<label for="box_edit_comment_author"></label>' +
						'<input id="box_edit_comment_author"' +
							' name="edit_comment_author" type="text" required>' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_edit_comment_email"></label>' +
						'<input id="box_edit_comment_email"' +
							' name="edit_comment_email" type="text">' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_edit_comment_website"></label>' +
						'<input id="box_edit_comment_website"' +
							' name="edit_comment_website" type="text">' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_edit_comment_message"></label>' +
						'<textarea id="box_edit_comment_message" name="edit_comment_message"' +
							' rows="10" cols="50" required class="focus" ></textarea>' +
					'</p>',

				buttons:
					'<input class="button" name="save" type="submit">'
			},
			box_comment_delete:
			{
				content: '<p class="message_info"></p>',
				buttons: '<input class="button" name="delete" type="submit">'
			}
		});

		// Texte localisé et attributs de formulaires.
		['author', 'email', 'website', 'message'].forEach(name =>
		{
			App.text(`[for="box_edit_comment_${name}"]`, BOX_COMMENT.l10n[name]);
			App.attr('#box_edit_comment_' + name, 'maxlength',
				BOX_COMMENT.form[name + '_maxlength']);
		});
		App.text('#box_comment_delete .message_info', BOX_COMMENT.l10n.delete_info);

		// Enregistrement des modifications.
		App.click('#box_comment_edit input[name="save"]', () =>
		{
			if (App.val('#box_edit_comment_message').trim() == '')
			{
				_q('#box_edit_comment_message').focus();
				return;
			}
			if (_commentGuest && App.val('#box_edit_comment_author').trim() == '')
			{
				_q('#box_edit_comment_author').focus();
				return;
			}

			const data =
			{
				section: 'comment-edit',
				com_id: _commentId,
				com_message: App.val('#box_edit_comment_message')
			};
			if (_commentGuest)
			{
				data.com_author = App.val('#box_edit_comment_author');
				data.com_email = App.val('#box_edit_comment_email');
				data.com_website = App.val('#box_edit_comment_website');
			}

			_ajax(data, r =>
			{
				if (_commentGuest)
				{
					if (r.com_author !== undefined)
					{
						_comment.user_name = r.com_author;
						App.val('#box_edit_comment_author', r.com_author);
						App.text(`[data-comment-id="${_commentId}"] .comment_name`, r.com_author);
					}
					if (r.com_email !== undefined)
					{
						_comment.user_email = r.com_email;
						App.val('#box_edit_comment_email', r.com_email);
					}
					if (r.com_website !== undefined)
					{
						_comment.user_website = r.com_website;
						App.val('#box_edit_comment_website', r.com_website);
						App.remove(`[data-comment-id="${_commentId}"] .comment_website`);
						if (r.com_website)
						{
							const website = App.createElement(
								'<span class="comment_website">(<a></a>)</span>');
							App.attr([website, 'a'], 'href', r.com_website);
							App.text([website, 'a'], BOX_COMMENT.l10n.website_link);
							App.append(`[data-comment-id="${_commentId}"] .comment_author`,
								website);
						}
					}
				}
				if (r.com_message !== undefined)
				{
					_comment.message = r.com_message;
					App.val('#box_edit_comment_message', r.com_message);
					App.html(`[data-comment-id="${_commentId}"] .comment_message`,
						r.com_message_formated);
				}
			});
		});

		// Suppression du commentaire.
		App.click('#box_comment_delete input[name="delete"]', () =>
		{
			if (confirm(BOX_COMMENT.l10n.delete_confirm))
			{
				_ajax(
				{
					section: 'comment-delete',
					com_id: _commentId
				},
				r =>
				{
					if (r.status == 'success')
					{
						alert(r.message);
						App.pageReload();
					}
				});
			}
		});
	}

	/**
	 * Boîte de sélecteur d'émojis.
	 *
	 * @return void
	 */
	function _createBoxEmoji()
	{
		// Création de la boîte.
		const menu = [], content = {}, emojis =
		{
			smileys_emotion: '&#x1F642;',
			people_body: '&#x1F590;&#xFE0F;',
			animals_nature: '&#x1F438;',
			food_drink: '&#x1F34E;',
			travel_places: '&#x1F30D;',
			activities: '&#x1F579;&#xFE0F;',
			objects: '&#x1F4F8;',
			symbols: '&#x26A0;&#xFE0F;'
		};
		for (const [name, code] of Object.entries(emojis))
		{
			menu.push({page_id: 'box_emoji_' + name, icon: code});
			content['box_emoji_' + name] = {content: ''};
		}
		_createBox(menu, content);
		App.addClass('#box_emoji .box_menu a span', 'emoji');

		// Insertion des émojis.
		function insert(name, list)
		{
			for (const code of list)
			{
				const a = App.createElement('<a href="javascript:;" class="emoji"></a>');
				App.append(a, (' ' + code).replaceAll(' ', '&#x'));
				App.click(a, function()
				{
					const position = _q('#message').selectionStart;
					const message = App.val('#message');
					const before = message.substring(0, position);
					const after  = message.substring(position, message.length);

					App.val('#message', before + App.text(this) + after);
					_q('#message').focus();
					_q('#message').selectionEnd = position + 2;

					_closeBox(0, false);
				});
				App.append(`#box_emoji_${name} .box_content`, a);
			}
		}

		// Première page.
		insert('smileys_emotion',
		[
			'1F600', '1F603', '1F604', '1F601', '1F606', '1F605', '1F923', '1F602', '1F642',
			'1F643', '1F609', '1F60A', '1F607', '1F970', '1F60D', '1F929', '1F618', '1F617',
			'1F61A', '1F619', '1F60B', '1F61B', '1F61C', '1F61D', '1F911', '1F917', '1F92D',
			'1F92B', '1F914', '1F910', '1F928', '1F610', '1F611', '1F636', '1F60F', '1F612',
			'1F644', '1F62C', '1F925', '1F60C', '1F614', '1F62A', '1F924', '1F634', '1F637',
			'1F912', '1F915', '1F922', '1F92E', '1F927', '1F975', '1F976', '1F974', '1F635',
			'1F92F', '1F920', '1F973', '1F60E', '1F913', '1F9D0', '1F615', '1F61F', '1F641',
			'1F62E', '1F62F', '1F632', '1F633', '1F97A', '1F626', '1F627', '1F628', '1F630',
			'1F625', '1F622', '1F62D', '1F631', '1F616', '1F623', '1F61E', '1F613', '1F629',
			'1F62B', '1F971', '1F624', '1F621', '1F620', '1F92C', '1F608', '1F47F', '1F480',
			'2620 FE0F', '1F4A9', '1F921', '1F479', '1F47A', '1F47B', '1F47D', '1F47E', '1F916',
			'1F63A', '1F638', '1F639', '1F63B', '1F63C', '1F63D', '1F640', '1F63F', '1F63E',
			'1F648', '1F649', '1F64A', '1F48C', '1F498', '1F49D', '1F496', '1F497', '1F493',
			'1F49E', '1F495', '1F49F', '2763 FE0F', '1F494', '2764 FE0F', '1F9E1', '1F49B',
			'1F49A', '1F499', '1F49C', '1F90E', '1F5A4', '1F90D', '1F48B', '1F4AF', '1F4A2',
			'1F4A5', '1F4AB', '1F4A6', '1F4A8', '1F573', '1F4AC', '1F4AD', '1F4A4'
		]);

		// Récupération des autres émojis.
		App.ajax({section: 'emojis'},
		{
			success: r =>
			{
				for (const [name, data] of Object.entries(r.emojis))
				{
					App.attr(`[data-box-page-id="box_emoji_${name}"]`, 'title', data.l10n);
					insert(name, data.list);
				}
			}
		});
	}

	/**
	 * Boîte de suppression des favoris.
	 *
	 * @return void
	 */
	function _createBoxFavoritesDelete()
	{
		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_favorites_delete_page',
				icon: '&#xe9ac;',
				text: BOX.l10n.delete_tab
			}
		],
		{
			box_favorites_delete_page:
			{
				content: '<p class="message_info"></p><br>',
				buttons: '<input class="button" name="delete" type="submit">'
			}
		});

		// Texte localisé.
		App.text('#box_favorites_delete_page .message_info', BOX_FAVORITES.l10n.delete_info);

		// Suppression des favoris.
		App.click('#box_favorites_delete_page input[name="delete"]', () =>
		{
			if (confirm(BOX_FAVORITES.l10n.delete_confirm))
			{
				_ajax(
				{
					section: 'favorites-remove-all',
					cat_id: CATEGORY.id
				},
				r =>
				{
					if (r?.message)
					{
						alert(r.message);
					}
					if (r?.status == 'success')
					{
						window.location = r.redirect;
					}
				});
			}
		});
	}

	/**
	 * Boîte d'édition d'un fichier.
	 *
	 * @return void
	 */
	function _createBoxItem()
	{
		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_item_infos',
				icon: '&#xe962;',
				text: BOX_EDIT.l10n.information
			},
			{
				page_id: 'box_item_tags',
				icon: '&#xf02c;',
				text: BOX_ITEM.l10n.tags,
				condition: BOX_ITEM.params.is_tags
			},
			{
				page_id: 'box_item_delete',
				icon: '&#xe9ac;',
				text: BOX.l10n.delete_tab
			}
		],
		{
			box_item_infos:
			{
				content:
					'<p class="field">' +
						'<label for="box_edit_item_title"></label>' +
						'<input id="box_edit_item_title" maxlength="255"' +
							' required type="text" class="focus">' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_edit_item_filename"></label>' +
						'<input id="box_edit_item_filename" maxlength="255"' +
							' required type="text">' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_edit_item_description"></label>' +
						'<textarea id="box_edit_item_description" rows="10" cols="50"' +
							' maxlength="65535"></textarea>' +
					'</p>',

				buttons:
					'<input class="button" name="save" type="submit">'
			},
			box_item_tags:
			{
				content:
					'<p class="field">' +
						'<label for="box_edit_item_tags"></label>' +
						'<textarea id="box_edit_item_tags" rows="10" cols="50"' +
							' maxlength="65535" class="focus"></textarea>' +
					'</p>',

				buttons:
					'<input class="button" name="save" type="submit">'
			},
			box_item_delete:
			{
				content: '<p class="message_info"></p>',
				buttons: '<input class="button" name="delete" type="submit">'
			}
		});

		// Texte localisé.
		App.text('[for="box_edit_item_title"]', BOX_EDIT.l10n.title);
		App.text('[for="box_edit_item_filename"]', BOX_ITEM.l10n.filename);
		App.text('[for="box_edit_item_description"]', BOX_EDIT.l10n.description);
		App.text('[for="box_edit_item_tags"]', BOX_ITEM.l10n.tags_field);
		App.text('#box_item_delete .message_info', BOX_ITEM.l10n.delete_info);

		// Données de formulaire.
		['title', 'filename', 'description', 'tags'].forEach(name =>
		{
			App.val('#box_edit_item_' + name, BOX_ITEM.form[name]);
		});

		// Modifications des informations.
		App.click('#box_item_infos [name="save"]', () =>
		{
			if (App.val('#box_edit_item_title').trim() == '')
			{
				_q('#box_edit_item_title').focus();
				return;
			}
			if (App.val('#box_edit_item_filename').trim() == '')
			{
				_q('#box_edit_item_filename').focus();
				return;
			}
			_ajax(
			{
				section: 'item-edit',
				item_id: ITEM.id,
				title: App.val('#box_edit_item_title'),
				filename: App.val('#box_edit_item_filename'),
				description: App.val('#box_edit_item_description')
			},
			Gallery.updateItem);
		});

		// Modifications des tags.
		App.click('#box_item_tags [name="save"]', () =>
		{
			_ajax(
			{
				section: 'item-edit',
				item_id: ITEM.id,
				tags: App.val('#box_edit_item_tags')
			},
			Gallery.updateItem);
		});

		// Suppression du fichier.
		App.click('#box_item_delete [name="delete"]', () =>
		{
			if (confirm(BOX_ITEM.l10n.delete_confirm))
			{
				_ajax(
				{
					section: 'item-delete',
					item_id: ITEM.id
				},
				r =>
				{
					if (r.status == 'success')
					{
						alert(r.message);
						window.location = r.redirect;
					}
				});
			}
		});
	}

	/**
	 * Boîte de connexion à un compte utilisateur.
	 *
	 * @return void
	 */
	function _createBoxLogin()
	{
		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_login_page',
				icon: '&#xe971;',
				text: BOX_LOGIN.l10n.login
			}
		],
		{
			box_login_page:
			{
				content:
					'<p class="field">' +
						'<label for="box_login_username"></label>' +
						'<input id="box_login_username" name="login"' +
							' required type="text" class="focus">' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_login_password"></label>' +
						'<input id="box_login_password" name="password"' +
							' required type="password">' +
					'</p>' +
					'<p class="field checkbox">' +
						'<input id="box_login_remember" name="remember" type="checkbox"> ' +
						'<label for="box_login_remember"></label>' +
					'</p>' +
					'<p id="box_connection_forgot" class="field">' +
						'<a></a>' +
					'</p>',

				buttons:
					'<input class="button" name="submit" type="submit">'
			}
		}, true);

		// Texte localisé.
		['username', 'password', 'remember'].forEach(name =>
		{
			App.text(`[for="box_login_${name}"]`, BOX_LOGIN.l10n[name]);
		});
		App.text('#box_connection_forgot a', BOX_LOGIN.l10n.forgot);

		// Attributs de formulaires.
		App.attr('#box_login_username', 'maxlength', BOX_LOGIN.form.username_maxlength);
		App.attr('#box_login_password', 'maxlength', BOX_LOGIN.form.password_maxlength);
		if (BOX.form.remember)
		{
			_q('#box_login_remember').checked = true;
		}
		App.attr('#box_connection_forgot a', 'href', App.link('forgot-password'));

		// Visionneur de mot de passe.
		Gallery.passwordViewer(_q('#box_login_password'));

		// Bouton "Annuler".
		App.click('#box_login [name="cancel"]', () =>
		{
			App.val('#box_login_username', '');
			App.val('#box_login_password', '');
		});

		// Envoi du formulaire.
		App.click('#box_login [name="submit"]', () =>
		{
			if (App.val('#box_login_username').trim() == '')
			{
				_q('#box_login_username').focus();
				return;
			}
			if (App.val('#box_login_password').trim() == '')
			{
				_q('#box_login_password').focus();
				return;
			}

			_ajax(
			{
				section: 'login',
				login: App.val('#box_login_username'),
				password: App.val('#box_login_password'),
				remember: _q('#box_login_remember').checked ? 1 : 0
			},
			r =>
			{
				if (r.status == 'success')
				{
					if (_q('#section_new_password') || _q('#section_register')
					 || _q('#section_validation') || _q('#section_login'))
					{
						document.location.href = GALLERY.path;
					}
					else
					{
						App.pageReload();
					}
				}
				else
				{
					_q('#box_login_username').focus();
					_boxReportTimeout = setTimeout(_reportRemove, 6000);
				}
			});
		});
	}

	/**
	 * Boîte de demande de mot de passe pour accéder à une catégorie.
	 *
	 * @return void
	 */
	function _createBoxPassword()
	{
		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_password_page',
				icon: '&#xe98d;',
				text: BOX.l10n.password
			}
		],
		{
			box_password_page:
			{
				content:
					'<p class="field">' +
						'<label for="box_cat_password"></label>' +
						'<input id="box_cat_password" name="password" type="password"' +
							' required size="40" maxlength="128" class="focus">' +
					'</p>' +
					'<p class="field checkbox">' +
						'<input id="box_cat_remember" name="remember" type="checkbox"> ' +
						'<label for="box_cat_remember"></label>' +
					'</p>',

				buttons:
					'<input class="button" name="submit" type="submit">'
			}
		}, true);

		// Texte localisé.
		App.text('[for="box_cat_password"]', BOX.l10n.password_auth);
		App.text('[for="box_cat_remember"]', BOX.l10n.remember);

		// Attributs de formulaires.
		if (BOX.form.remember)
		{
			_q('#box_cat_remember').checked = true;
		}

		// Visionneur de mot de passe.
		Gallery.passwordViewer(_q('#box_cat_password'));

		// Bouton "Annuler".
		App.click('#box_password [name="cancel"]', () =>
		{
			App.val('#box_cat_password', '');
		});

		// Envoi du formulaire.
		App.click('#box_password [name="submit"]', () =>
		{
			if (App.val('#box_cat_password').trim() == '')
			{
				_q('#box_cat_password').focus();
				return;
			}

			_ajax(
			{
				section: 'cat-password',
				password: App.val('#box_cat_password'),
				remember: _q('#box_cat_remember').checked ? 1 : 0,
				cat_id: App.attr(_button, 'data-id')
					 || _button.closest('li')?.id.split(/[\{\}]/)[1]
			},
			r =>
			{
				if (r.status == 'success')
				{
					window.location = r.url;
				}
				else
				{
					_q('#box_cat_password').focus();
					_boxReportTimeout = setTimeout(_reportRemove, 6000);
				}
			});
		});
	}

	/**
	 * Boîte du mode sélection.
	 *
	 * @return void
	 */
	function _createBoxSelection()
	{
		const l10n = BOX_SELECTION.l10n;
		const params = BOX_SELECTION.params;

		// Page des options de sélection.
		let selection_options_content =
			'<p class="field selection_title"></p>' +
			'<p class="message_info"></p>';
		if (params.tools)
		{
			selection_options_content +=
				'<br>' +
				'<p class="field select_tool">' +
					'<a class="add" href="javascript:;">' +
						'<span>&#xf067;</span><span></span>' +
					'</a>' +
				'</p>' +
				'<p class="field select_tool">' +
					'<a class="remove" href="javascript:;">' +
						'<span>&#xf068;</span><span></span>' +
					'</a>' +
				'</p>';
		}

		// Création de la boîte.
		_createBox(
		[
			{
				page_id: 'box_selection_options',
				icon: '&#xea52;'
			},
			{
				page_id: 'box_selection_download',
				icon: '&#xe9c7;',
				title: l10n.download
			},
			{
				page_id: 'box_selection_favorites',
				icon: '&#xe910;',
				title: l10n.favorites,
				condition: params.is_favorites
			},
			{
				page_id: 'box_selection_tags',
				icon: '&#xf02c;',
				title: l10n.tags,
				condition: params.is_admin && params.is_tags
			},
			{
				page_id: 'box_selection_move',
				icon: '&#xe933;',
				title: l10n.move,
				condition: params.is_admin && params.is_album
			},
			{
				page_id: 'box_selection_delete',
				icon: '&#xe9ac;',
				title: BOX.l10n.delete_tab,
				condition: params.is_admin
			}
		],
		{
			box_selection_options:
			{
				content: selection_options_content,
				buttons:
					'<input id="box_selection_button" class="button" type="submit">' +
					'<input disabled class="action button disabled" name="delete" type="submit">'
			},
			box_selection_download:
			{
				content:
					'<p class="field selection_title"></p>' +
					'<p class="message_info" id="selection_filesize">&nbsp;</p>',

				buttons:
					'<input disabled class="action button disabled"' +
						' name="download" type="submit">'
			},
			box_selection_favorites:
			{
				content:
					'<p class="field selection_title"></p>' +
					'<p class="field">' +
						'<input id="box_selection_favorites_add" type="radio"' +
							' name="box_selection_favorites_action" value="add" checked> ' +
						'<label for="box_selection_favorites_add"></label>' +
					'</p>' +
					'<p class="field">' +
						'<input id="box_selection_favorites_remove" type="radio"' +
							' name="box_selection_favorites_action" value="remove"> ' +
						'<label for="box_selection_favorites_remove"></label>' +
					'</p>',

				buttons:
					'<input disabled class="action button disabled" name="save" type="submit">'
			},
			box_selection_tags:
			{
				content:
					'<p class="field selection_title"></p>' +
					'<p class="field">' +
						'<input type="checkbox" id="box_selection_delete_tags_all"> ' +
						'<label for="box_selection_delete_tags_all"></label>' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_selection_delete_tags"></label>' +
						'<textarea id="box_selection_delete_tags" rows="3" cols="50"' +
							' maxlength="65535"></textarea>' +
					'</p>' +
					'<p class="field">' +
						'<label for="box_selection_add_tags"></label>' +
						'<textarea id="box_selection_add_tags" rows="3" cols="50"' +
							' maxlength="65535" class="focus"></textarea>' +
					'</p>',

				buttons:
					'<input disabled class="action button disabled" name="save" type="submit">'
			},
			box_selection_move:
			{
				content:
					'<p class="field selection_title"></p>' +
					'<p class="message_info"></p>',

				buttons:
					'<input disabled class="action button disabled" name="move" type="submit">'
			},
			box_selection_delete:
			{
				content:
					'<p class="field selection_title"></p>' +
					'<p class="message_info"></p>',

				buttons:
					'<input disabled class="action button disabled" name="delete" type="submit">'
			}
		});

		// Texte localisé : options.
		App.text('#box_selection_options .selection_title', l10n.selection);
		App.text('#box_selection_options .message_info', l10n.selection_info);
		App.text('#box_selection .select_tool .add span + span', l10n.add_all);
		App.text('#box_selection .select_tool .remove span + span', l10n.remove_all);
		App.val('#box_selection_button', l10n[params.start ? 'stop' : 'start']);
		App.val('#box_selection_options [name="delete"]', l10n.delete_selection);

		// Texte localisé : téléchargement.
		App.text('#box_selection_download .selection_title', l10n.download);
		App.val('#box_selection_download [name="download"]', l10n.download_button);

		// Texte localisé : favoris.
		App.text('#box_selection_favorites .selection_title', l10n.favorites);
		App.text('[for="box_selection_favorites_add"]', l10n.favorites_add);
		App.text('[for="box_selection_favorites_remove"]', l10n.favorites_remove);

		// Texte localisé : tags.
		App.text('#box_selection_tags .selection_title', l10n.tags);
		App.text('[for="box_selection_delete_tags_all"]', l10n.delete_tags_all);
		App.text('[for="box_selection_delete_tags"]', l10n.delete_tags);
		App.text('[for="box_selection_add_tags"]', l10n.add_tags);

		// Texte localisé : déplacement.
		App.text('#box_selection_move .selection_title', l10n.move);
		App.text('#box_selection_move .message_info', l10n.move_info);
		App.val('#box_selection_move [name="move"]', l10n.move_button);

		// Texte localisé : suppression.
		App.text('#box_selection_delete .selection_title', BOX.l10n.delete_tab);
		App.text('#box_selection_delete .message_info', l10n.delete_info);

		// Attributs de formulaires.
		App.attr('#box_selection_button', 'name', params.start ? 'stop' : 'start');

		// Démarrer / arrêter le mode sélection.
		App.click('#box_selection_button', () =>
		{
			_closeBox(_animateDuration, true, () =>
			{
				const start = params.start;
				params.start = !start;
				App.attr('#box_selection_button', 'name', start ? 'start' : 'stop');
				App.val('#box_selection_button', l10n[start ? 'start' : 'stop']);
				App.toggleClass('.thumbs_items', 'selectable', !start);
				App.ajax(
				{
					section: 'cookie-prefs-write',
					param: 'selection_start',
					value: start ? '0' : '1'
				});
			});
		});

		// Vider la sélection.
		App.click('#box_selection_options [name="delete"]', () =>
		{
			App.ajax(
			{
				section: 'selection',
				action: 'selection-delete'
			},
			{
				error: r => alert('error: ' + r.message),
				success: r =>
				{
					App.removeClass('.thumbs_items dl', 'selected');
					App.html('.thumbs_items dl .selection_icon', '&#xea53;');
					Gallery.updateSelection(r.stats);
					Gallery.diaporama = null;
				}
			});
		});

		// Télécharger la sélection.
		App.click('#box_selection_download [name="download"]', () =>
		{
			if (params.stats.count > 0)
			{
				fetch(GALLERY.path + '/ajax.php',
				{
					body: JSON.stringify(
					{
						anticsrf: GALLERY.anticsrf,
						section: 'selection',
						action: 'download'
					}),
					method: 'POST',
					headers: {'Content-Type': 'application/json;'},
				})
				.then(response => response.blob())
				.then(response =>
				{
					const a = document.createElement("a");
					const url = URL.createObjectURL(response);
					a.href = url;
					a.download = 'selection.zip';
					a.click();
					window.URL.revokeObjectURL(url);
				});
			}
		});

		// Ajouter dans les favoris ou retirer des favoris.
		App.click('#box_selection_favorites [name="save"]', () =>
		{
			const action = _q('#box_selection_favorites_add').checked ? 'add' : 'remove';

			_ajax(
			{
				section: 'selection',
				action: 'favorites-' + action
			},
			r =>
			{
				if (r.status == 'success')
				{
					Gallery.diaporama = null;
				}
			});
		});

		// Ajouter ou supprimer des tags.
		App.on('#box_selection_delete_tags_all', 'change', function()
		{
			App.toggleClass(_q('#box_selection_delete_tags').closest('p'),
				'disabled', this.checked);
			_q('#box_selection_delete_tags').disabled = this.checked;
		});
		App.click('#box_selection_tags [name="save"]', () =>
		{
			const delete_all = _q('#box_selection_delete_tags_all').checked;

			if (delete_all && !confirm(l10n.delete_tags_all_confirm))
			{
				return;
			}

			_ajax(
			{
				section: 'selection',
				action: 'tags',
				add: App.val('#box_selection_add_tags'),
				delete: delete_all ? '' : App.val('#box_selection_delete_tags'),
				delete_all: delete_all ? 1 : 0
			},
			r =>
			{
				if (r.status == 'success')
				{
					Gallery.diaporama = null;
				}
			});
		});

		// Déplacer les fichiers sélectionnés dans l'album actuel.
		App.click('#box_selection_move [name="move"]', () =>
		{
			_ajax(
			{
				section: 'selection',
				action: 'move',
				album_id: CATEGORY.id
			},
			r =>
			{
				if (r.status == 'success')
				{
					Gallery.diaporama = null;
				}
			});
		});

		// Supprimer les fichiers.
		App.click('#box_selection_delete [name="delete"]', () =>
		{
			if (confirm(l10n.delete_confirm))
			{
				_ajax(
				{
					section: 'selection',
					action: 'delete'
				},
				r =>
				{
					if (r.status == 'success')
					{
						Gallery.updateSelection(r.stats);
						Gallery.diaporama = null;
					}
				});
			}
		});

		// Tout (dé)sélectionner sur la page courante.
		App.click('#box_selection_options .select_tool a', function()
		{
			const action = App.attr(this, 'class');
			const id = [];

			App.each('#page_thumbs dl', elem =>
			{
				if (action == 'add' && !App.hasClass(elem, 'selected')
				 || action != 'add' && App.hasClass(elem, 'selected'))
				{
					id.push(parseInt(App.attr(elem, 'data-id')));
				}
				App.toggleClass(elem, 'selected', action == 'add');
				App.html([elem, '.selection_icon'], action == 'add' ? '&#xea52;' : '&#xea53;');
			});

			if (!id.length)
			{
				return;
			}

			App.ajax(
			{
				section: 'selection',
				action: 'selection-' + action,
				id: id
			},
			{
				error: r => alert('error: ' + r.message),
				success: r =>
				{
					Gallery.updateSelection(r.stats);
					Gallery.diaporama = null;
				}
			});
		});

		Gallery.updateSelection(params.stats);
	}

	/**
	 * Gestion du clavier.
	 *
	 */
	function _keyUpBox(evt)
	{
		if (evt.keyCode == 27)
		{
			_q(_boxPage, '[name="cancel"]').click();
		}
	}

	/**
	 * Icône de chargement.
	 *
	 */
	function _loadingStart()
	{
		if (!_q('#box_loading'))
		{
			App.each([_boxPage, '.box_buttons input[type="submit"]'],
				elem => elem.disabled = true);
			App.append([_boxPage, '.box_buttons'], '<span id="box_loading"></span>');
		}
	}

	function _loadingStop()
	{
		App.remove('#box_loading');
		App.each([_boxPage, '.box_buttons input[type="submit"]:not(.disabled)'],
			elem => elem.disabled = false);
		if (_boxAjax)
		{
			_boxAjax.abort();
		}
	}

	/**
	 * Édition d'un commentaire : remplissage du formulaire à l'ouverture de la boîte.
	 *
	 * @return void
	 */
	function _openBoxComment()
	{
		_commentId = App.attr(_button.closest('.comment'), 'data-comment-id');
		_commentGuest = App.attr(`[data-comment-id="${_commentId}"]`, 'data-user-id') == 2;
		_comment = BOX_COMMENT.edit[_commentId];

		const fields = '#box_comment_edit p.field:nth-child(-n + 3)';

		if (_commentGuest)
		{
			App.val('#box_edit_comment_author', _comment.user_name);
			App.val('#box_edit_comment_email', _comment.user_email);
			App.val('#box_edit_comment_website', _comment.user_website);

			App.show(fields);
		}
		else
		{
			App.hide(fields);
		}

		App.val('#box_edit_comment_message', _comment.message);
	}

	/**
	 * Messages de rapport.
	 *
	 */
	function _reportRemove()
	{
		App.hide('#box_report', _animateDuration, () =>
		{
			App.remove('#box_report');
			_loadingStop();
		});
	}

	function _reportShow(type, text)
	{
		App.remove('#box_loading');

		// Création du message.
		App.append([_boxPage, '.box_buttons'], '<div id="box_report"><span></span></div>');
		App.addClass('#box_report span', 'message_' + type);
		App.text('#box_report span', text);
		App.click('#box_report', () =>
		{
			clearTimeout(_boxReportTimeout);
			_reportRemove();
		});

		// Affichage du message.
		App.show('#box_report', _animateDuration, 'flex');
	
		// Suppression du message au bout de 2 secondes, sauf pour les erreurs.
		if (type != 'error')
		{
			_boxReportTimeout = setTimeout(_reportRemove, 2000);
		}
	}
}



/**
 * Gestion des boîtes flottantes "fléchées"
 * et de l'affichage du menu principal.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
function BoxesArrow()
{
	// Raccourcis.
	const _q = App.q, _qAll = App.qAll;



	// Durée d'animation pour l'ouverture et la fermeture des boîtes.
	var _animateDuration = 250;

	// Propriétés pour chaque boîte.
	var _boxName, _boxOpen = false, _boxTimeout = [];

	// Propriétés pour la liste des albums.
	var _browseHeightMargin = 100;

	// Boutton qui a ouvert une boîte.
	var _button;

	// Indique si le menu principal est ouvert.
	var _menuOpen = false;



	// Fabrique la liste des catégories.
	this.browse = callback => _htmlBrowse(callback);



	/**
	 * Initialisation.
	 *
	 * @return void
	 */
	(function()
	{
		// Ouverture ou fermeture d'une boîte par un click sur un élément.
		App.on('[data-box-arrow]', 'click', function(evt)
		{
			_boxOpen = false;
			if (_boxName == 'browse')
			{
				App.removeClass('#menu_gallery', 'open');
			}

			_boxName = App.attr(this, 'data-box-arrow');
			_button = this;

			// Création du code HTML de la boîte.
			switch (_boxName)
			{
				case 'browse' :
					_htmlBrowse();
					break;
				case 'search' :
					_htmlSearch();
					break;
			}

			// On désactive la liste des albums sur les écrans de petites largeurs.
			if (_menuOpen && _boxName == 'browse' && Gallery.isSmallScreen())
			{
				return;
			}

			for (const id in _boxTimeout)
			{
				clearTimeout(_boxTimeout[id]);
			}

			// Fermeture de la boîte.
			if (_q('#' + _boxName)?.checkVisibility())
			{
				_closeBox();
			}

			// Ouverture de la boîte.
			else
			{
				// Fermeture de toutes les boîtes.
				_closeBoxes();

				// Si le menu est ouvert, on le ferme.
				if (_q('#menu nav').checkVisibility())
				{
					_closeMenu();
				}

				// Ajout de la flèche.
				if (_boxName != 'user_menu')
				{
					if (!_q(`#${_boxName} .arrow_top`))
					{
						App.append('#' + _boxName,
							'<div class="arrow_top_border"></div><div class="arrow_top"></div>');
					}

					// Positionnement de la boîte.
					_positionBox();
				}

				// Ouverture de la boîte.
				_q(`#${_boxName} input:first-child`)?.focus();
				App.show('#' + _boxName, _animateDuration, 'block', () =>
				{
					_boxOpen = true;
					App.on(window,
					{
						'click': _clickBox,
						'keydown': _keyDownBox,
						'keyup': _keyUpBox,
						'resize': _resizeBox
					});
				});
				if (_boxName == 'browse')
				{
					_resizeBrowse();
					_scrollBrowse();
					App.addClass('#menu_gallery', 'open');
				}
			}

			if (_q('#' + _boxName))
			{
				evt.preventDefault();
			}
		});

		// Gestion des événements pour le menu utilisateur.
		_addEvents('#user_menu');

		// Gestion du menu principal.
		_menu();
	})();



	/**
	 * Ajouts de gestionnaires d'événements sur une boîte.
	 *
	 * @param string elem
	 *
	 * @return void
	 */
	function _addEvents(elem)
	{
		App.on(elem, 'click', evt => evt.stopPropagation());
	}

	/**
	 * Gestion de la fermeture des boîtes.
	 *
	 */
	function _closeBox()
	{
		if (!Gallery.isSmallScreen() && _boxName == 'user_menu')
		{
			return;
		}

		if (_boxName == 'browse')
		{
			App.removeClass('#menu_gallery', 'open');
		}
		App.hide('#' + _boxName, _animateDuration);

		App.off(window,
		{
			'click': _clickBox,
			'keydown': _keyDownBox,
			'keyup': _keyUpBox,
			'resize': _resizeBox
		});

		_boxOpen = false;
	}

	function _closeBoxes(boxes = ['user_menu'])
	{
		App.each('.box_arrow', elem =>
		{
			if (elem.checkVisibility() && !boxes.includes(elem.id))
			{
				App.hide(elem, _animateDuration);
			}
		});
	}

	function _closeMenu()
	{
		if (!Gallery.isSmallScreen())
		{
			return;
		}

		App.html('#menu_link', '&#xe966;');
		App.removeClass('main', 'dark');
		App.hide('#menu nav', _animateDuration, () =>
		{
			App.off(window, {'click': _clickMenu, 'keyup': _keyUpMenu});
			_menuOpen = false;
		});
	}

	/**
	 * Gestion des événements "click".
	 *
	 */
	function _clickBox()
	{
		if (_boxName == 'browse' && _q('#box_password')?.checkVisibility())
		{
			return;
		}
		if (_boxOpen)
		{
			_closeBox();
		}
	}

	function _clickMenu()
	{
		if (_menuOpen)
		{
			_closeMenu();
		}
	}

	/**
	 * Insertion du code HTML de la liste des albums.
	 
	 * @param function callback
	 *
	 * @return void
	 */
	function _htmlBrowse(callback)
	{
		if (_q('#browse'))
		{
			return;
		}

		// Création de la liste.
		App.append('#content',
			'<div id="browse" class="box_arrow" data-levels="2">' +
				'<div id="browse_inner">' +
					'<p id="browse_search" class="field"><input name="search" type="text"></p>' +
					'<ul tabindex="-1"></ul>' +
					'<p id="browse_path"><span>&nbsp;</span></p>' +
				'</div>' +
			'</div>');

		function make_list(browse)
		{
			let levels = 1, subcats = 0;
			for (const i of browse.list)
			{
				const cat_id = i[0].replace(/^.+\{(\d+)\}.+$/, '$1');
				const level = parseInt(i[0].replace(/^.*:(\d+)\{.+$/, '$1'));
				if (level > levels)
				{
					levels = level;
				}

				// Élément de la liste.
				const li = document.createElement('li');
				li.id = i[0];

				// Icône pour déplier les sous-catégories.
				if (i[0].match('s') || cat_id == 1)
				{
					if (cat_id != 1)
					{
						subcats = 1;
					}
					App.append(li, '<i>+</i>');
				}

				// Lien.
				const link = document.createElement('a');
				const type = i[0].match('s') || cat_id == 1 ? 'category' : 'album';
				link.href = browse.url.replace(
					'category/id-urlname',
					`${type}/${cat_id}-${i[2]}`
				) + '#menu';
				if (i[0].match('l'))
				{
					App.attr(link, 'data-box', 'password');
				}

				// Titre.
				const title = document.createElement('b');
				App.text(title, i[1]);
				App.append(link, title);

				// Nombre de fichiers.
				if (i[3] !== undefined && i[3] != 0)
				{
					const count = document.createElement('i');
					App.text(count, i[3]);
					App.append(link, count);
				}

				App.append(li, link);
				App.append('#browse ul', li);
			}

			App.on('#browse [data-box]', 'click', Gallery.boxes.open);

			App.attr('#browse', {'data-levels': levels, 'data-subcats': subcats});

			// Catégorie courante.
			if (_q('#browse li[id*="c"]'))
			{
				const cat_infos = get_infos(_q('#browse li[id*="c"]'));
				if (cat_infos.id > 1 && cat_infos.parents)
				{
					let id, parents = cat_infos.parents + '.' + cat_infos.id;
					while (parents.match(/\./))
					{
						parents = parents.replace(/\.\d+$/, '');
						id = parents.replace(/^.*\.(\d+)$/, '$1');
						App.each(`#browse li[id$="}${parents}"]`, elem =>
						{
							App.attr(elem, 'id', 'v' + elem.id.replace('v', ''));
							elem.style.display = 'flex';
						});
						if ((id == 1 && cat_infos.level > 1) || id != 1)
						{
							App.text(`#browse li[id*="{${id}}"] > i`, '-');
						}
					}
				}
			}
			_scrollBrowse();

			// Gestion des "+" et des "-".
			App.click('#browse li > i', function()
			{
				const i = get_infos(this.closest('li'));
				const is_m = App.text(this) == '-';
				const parents = i.parents + '.' + i.id;
				const each = elem =>
				{
					App.attr(elem, 'id', (is_m ? '' : 'v') + elem.id.replace('v', ''));
					elem.style.display = is_m ? 'none' : 'flex';
				};

				App.text(this, is_m ? '+' : '-');

				if (i.id == '1')
				{
					App.each('#browse li:not([id*=":0{"]):not([id*=":1{"])', each);
					App.text('#browse li > i', is_m ? '+' : '-');
				}
				else
				{
					App.each(`#browse li[id${is_m ? '*' : '$'}="}${parents}"]`, each);
					App.text(`#browse li[id*="${is_m ? `}${parents}` : '{1}'}"] > i`,
						is_m ? '+' : '-');
				}

				if (![..._qAll('#browse li:not([id*="{1}"]) > i')]
				.find(elem => App.text(elem) == '-'))
				{
					App.text('#browse li[id*="{1}"] > i', '+');
				}

				_q('#browse_search input').focus();
			});

			// Chemin complet de la catégorie.
			function enter()
			{
				const elem = this.tagName.toLowerCase() == 'a' ? this.closest('li') : this;
				const i = get_infos(elem);
				const path = [];
				if (i.id > 1)
				{
					i.parents.split('.').forEach(id =>
					{
						path.push(App.text(`#browse li[id*="{${id}}"] b`));
					});
				}
				path.push(App.text([elem, 'b']));
				App.text('#browse_path span', path.join(' / '));
			}
			function leave()
			{
				App.html('#browse_path', '<span>&nbsp;</span>');
			}
			App.on('#browse li', {'mouseenter': enter, 'mouseleave': leave});
			App.on('#browse li a', {'blur': leave, 'focus': enter});

			callback?.();
		}

		if (typeof BROWSE == 'undefined')
		{
			App.ajax(
			{
				section: 'albums-list',
				q: GALLERY.q_pageless
			},
			{
				error: r => console.log('error: ' + r.message),
				success: r =>
				{
					make_list(r.browse);
				}
			});
		}
		else
		{
			make_list(BROWSE);
		}

		// Barre de recherche.
		function get_infos(elem)
		{
			const i = elem.id.split(/[\{\}]/);
			return {
				id: i[1],
				level: i[0].replace(/\D+/, ''),
				parents: i[2]
			};
		};
		App.val('#browse_search input', '');
		App.on('#browse_search', 'input', function()
		{
			let search = _q(this, 'input').value;
			search = App.regexpEscape(search);
			search = App.regexpAccents(search);
			search = search.replace(/\s+/g, '\\s+');
			const regexp = new RegExp(search, 'i');
			App.hide('#browse li');
			App.each('#browse li:not(:first-child)', elem =>
			{
				if (regexp.test(App.text([elem, 'b'])))
				{
					App.show(elem, 'flex');
				}
			});
			App.toggleClass('#browse', 'search', search !== '');
			if (search === '')
			{
				App.show('#browse li[id*="v"]', 'flex');
				App.hide('#browse li:not([id*="v"])');
			}
			if (_q('#browse[data-subcats="1"]'))
			{
				_q('#browse_path').style.display = search === '' ? 'none' : 'block';
				if (search === '')
				{
					_browseHeightMargin = 100;
				}
				else if (_q('#browse_path'))
				{
					_browseHeightMargin = 200;
				}
				_resizeBrowse();
			}
		});

		// Autres événements.
		_addEvents('#browse');
	}

	/**
	 * Insertion du code HTML du moteur de recherche.
	 *
	 * @return void
	 */
	function _htmlSearch()
	{
		if (_q('#search'))
		{
			return;
		}

		// Création de la boîte.
		App.append('#content',
			'<div id="search" class="box_arrow">' +
				'<form method="post" autocomplete="off">' +
					'<input required name="search_query" maxlength="68" type="text">' +
					'<input name="anticsrf" type="hidden">' +
					'<input class="button" type="submit">' +
				'</form>' +
			'</div>');

		App.attr('#search form', 'action', App.link(GALLERY.q_pageless));
		App.val('#search input[name="anticsrf"]', GALLERY.anticsrf);
		App.val('#search input[type="submit"]', SEARCH.l10n.submit);

		if (window.location.href.match('/search/'))
		{
			App.val('#search input[name="search_query"]',
				App.text('#breadcrumb_tools .filter_name') || '');
		}

		// Recherche avancée.
		if (SEARCH.params.advanced)
		{
			const a = document.createElement('a');
			App.attr(a, 'href', App.link('search-advanced'));
			App.text(a, SEARCH.l10n.advanced);
			App.append('#search', a);
		}

		// Suggestions de recherche.
		if (SEARCH.params.suggestion)
		{
			let ajax, search_timeout;
			function make_suggest_list(val, r)
			{
				App.remove('#search ul');
				if (r.suggestion.length)
				{
					const ul = App.createElement('<ul id="search_suggest"></ul>');
					for (const term of r.suggestion)
					{
						const li = App.createElement('<li><a href="javascript:;"></a></li>');
						App.text([li, 'a'], term);
						App.click([li, 'a'], function()
						{
							App.val('#search input[name="search_query"]', `"${App.text(this)}"`);
							App.trigger('#search form', 'submit');
						});
						ul.append(li);
					}
					App.append('#search', ul);
				}
			}
			App.on('#search input[name="search_query"]', 'input', function()
			{
				if (ajax)
				{
					ajax.abort();
				}
				clearTimeout(search_timeout);
				search_timeout = setTimeout(() =>
				{
					const val = this.value;
					if (val)
					{
						ajax = App.ajax(
						{
							section: 'search-suggestion',
							search: val
						},
						{
							success: r => make_suggest_list(val, r)
						},
						() => App.remove('#search ul'));
					}
					else
					{
						App.remove('#search ul');
					}
				}, 50);
			});
		}

		_addEvents('#search');
	}

	/**
	 * Gestion du clavier.
	 *
	 */
	function _keyDownBox(evt)
	{
		if (_boxOpen)
		{
			switch (evt.keyCode)
			{
				// Gauche.
				case 37 :
					if (_boxName == 'browse' && document.activeElement.closest('ul')
					&& !_q('#browse_path').checkVisibility())
					{
						evt.preventDefault();
						const i = document.activeElement.previousSibling;
						if (i?.textContent == '-')
						{
							i.click();
							i.nextSibling.focus();
						}
					}
					break;

				// Haut.
				case 38 :
					evt.preventDefault();

					// Liste des albums.
					if (_boxName == 'browse')
					{
						if (document.activeElement.closest('ul'))
						{
							let prev = document.activeElement.closest('li').previousSibling;
							if (prev)
							{
								while (prev.tagName.toLowerCase() == 'li')
								{
									if (prev.checkVisibility())
									{
										_q(prev, 'a').focus();
										break;
									}
									else
									{
										prev = prev.previousSibling;
										if (!prev)
										{
											_q('#browse input[name="search"]').focus();
											break;
										}
									}
								}
							}
							else
							{
								_q('#browse input[name="search"]').focus();
							}
						}
						else
						{
							const elem = [..._qAll('#browse li')]
								.reverse().find(e => e.checkVisibility());
							if (elem)
							{
								_q(elem, 'a').focus();
							}
						}
					}

					// Suggestion de recherche.
					if (_boxName == 'search' && _q('#search ul'))
					{
						if (document.activeElement.closest('#search_suggest'))
						{
							const prev = document.activeElement.closest('li').previousSibling;
							if (prev)
							{
								prev.querySelector('a').focus();
							}
							else
							{
								_q('#search input[name="search_query"]').focus();
							}
						}
						else
						{
							_q('#search li:last-child a').focus();
						}
					}
					break;

				// Droite.
				case 39 :
					if (_boxName == 'browse' && document.activeElement.closest('ul')
					&& !_q('#browse_path').checkVisibility())
					{
						evt.preventDefault();
						const i = document.activeElement.previousSibling;
						if (i?.textContent == '+')
						{
							i.click();
							i.nextSibling.focus();
						}
					}
					break;

				// Bas.
				case 40 :
					evt.preventDefault();

					// Liste des albums.
					if (_boxName == 'browse')
					{
						if (document.activeElement.closest('ul'))
						{
							let next = document.activeElement.closest('li').nextSibling;
							if (next)
							{
								while (next.tagName.toLowerCase() == 'li')
								{
									if (next.checkVisibility())
									{
										_q(next, 'a').focus();
										break;
									}
									else
									{
										next = next.nextSibling;
										if (!next)
										{
											_q('#browse input[name="search"]').focus();
											break;
										}
									}
								}
							}
							else
							{
								_q('#browse input[name="search"]').focus();
							}
						}
						else
						{
							const elem = [..._qAll('#browse li')].find(e => e.checkVisibility());
							if (elem)
							{
								_q(elem, 'a').focus();
							}
						}
					}

					// Suggestion de recherche.
					if (_boxName == 'search' && _q('#search ul'))
					{
						if (document.activeElement.closest('#search_suggest'))
						{
							const next = document.activeElement.closest('li').nextSibling;
							if (next)
							{
								_q(next, 'a').focus();
							}
							else
							{
								_q('#search input[name="search_query"]').focus();
							}
						}
						else
						{
							_q('#search li:first-child a').focus();
						}
					}

					break;
			}
		}
	}

	function _keyUpBox(evt)
	{
		if (_boxOpen && evt.keyCode == 27)
		{
			_closeBox();
			_button.focus();
		}
	}

	function _keyUpMenu(evt)
	{
		if (_menuOpen && evt.keyCode == 27)
		{
			_closeMenu();
		}
	}

	/**
	 * Gestion de l'affichage du menu.
	 *
	 * @return void
	 */
	function _menu()
	{
		App.click('#menu_link', () =>
		{
			_menuOpen = false;
			if (_q('#menu nav').checkVisibility())
			{
				_closeMenu();
			}
			else
			{
				_closeBoxes([]);

				App.html('#menu_link', '&#xe916;');
				App.addClass('main', 'dark');
				App.show('#menu nav', _animateDuration, 'block', () =>
				{
					App.on(window, {'click': _clickMenu, 'keyup': _keyUpMenu});
					_menuOpen = true;
				});
			}
		});
		App.on('#menu nav', 'click', evt =>
		{
			if (_menuOpen)
			{
				evt.stopPropagation();
			}
		});
	}

	/**
	 * Positionnement de la boîte.
	 *
	 * @param int opacity
	 *
	 * @return void
	 */
	function _positionBox(opacity = 0)
	{
		if (_boxName == 'user_menu')
		{
			return;
		}

		App.style('#' + _boxName, {opacity: opacity, display: 'block'});

		const margin = 15;
		const box_width = _q('#' + _boxName).offsetWidth;
		const top = App.offset(_button).bottom + 20
			+ (parseInt(getComputedStyle(_button)['margin-bottom']) || 0);

		let left = (App.offset(_button).left + (_button.offsetWidth / 2)) - (box_width / 2);
		const previous_left = left;
		const arrow_width = App.offset(`#${_boxName} .arrow_top`).width;

		let arrow_left = ((box_width / 2) - (arrow_width / 2));
		const gallery_left = _q('#gallery').offsetLeft + margin;
		const gallery_right = gallery_left + _q('#gallery').offsetWidth - (margin * 2);

		if (left < gallery_left)
		{
			left = gallery_left;
		}
		else if (left + box_width > gallery_right)
		{
			left = gallery_right - box_width;
		}
		arrow_left -= left - previous_left;

		App.style(`#${_boxName} div[class^="arrow"]`, {left: arrow_left + 'px'});
		App.style('#' + _boxName, {top: top + 'px', left: left + 'px'});
	}

	/**
	 * Modification de l'affichage d'une boîte lors du redimensionnement
	 * de la fenêtre du navigateur ou d'une action particulière.
	 *
	 */
	function _resizeBox()
	{
		if (_boxOpen)
		{
			if (Gallery.isSmallScreen())
			{
				_closeBox();
			}
			else
			{
				_positionBox(1);
				_resizeBrowse();
			}
		}
	}

	function _resizeBrowse()
	{
		if (_boxName != 'browse')
		{
			return;
		}

		const available_height = document.documentElement.clientHeight;
		const top = Math.round(_q('#menu_gallery').getBoundingClientRect().bottom);
		const max_height = available_height - top - _browseHeightMargin;
		_q('#browse ul').style.maxHeight = (max_height > 600 ? 600 : max_height) + 'px';
	}

	/**
	 * Fait défiler la liste des catégories au niveau de la catégorie courante.
	 *
	 * @return void
	 */
	function _scrollBrowse()
	{
		const list = _q('#browse_inner ul');
		const current = _q(list, 'li[id*="c"]');
		if (current)
		{
			const n = Math.floor((list.offsetHeight / current.offsetHeight) / 3);
			list.scroll({top: current.offsetTop - list.offsetTop - (n * current.offsetHeight)});
		}
	}
}