mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 17:13:08 +00:00 
			
		
		
		
	Format with biome instead of standard
This commit is contained in:
		| @@ -8,10 +8,11 @@ repos: | ||||
|         args: ["--fix", "--silent"] | ||||
|       # Run the formatter. | ||||
|       - id: ruff-format | ||||
|   - repo: https://github.com/standard/standard | ||||
|     rev: v17.1.2 | ||||
|   - repo: https://github.com/biomejs/pre-commit | ||||
|     rev: "v0.1.0"  # Use the sha / tag you want to point at | ||||
|     hooks: | ||||
|       - id: standard | ||||
|       - id: biome-check | ||||
|         additional_dependencies: ["@biomejs/biome@1.9.3"] | ||||
|   - repo: https://github.com/rtts/djhtml | ||||
|     rev: 3.0.6 | ||||
|     hooks: | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| { | ||||
|   "presets": [ | ||||
|     [ | ||||
|       "@babel/preset-env", | ||||
|       { | ||||
|         "targets": { | ||||
|           "edge": "17", | ||||
|           "firefox": "60", | ||||
|           "chrome": "67", | ||||
|           "safari": "11.1" | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   ] | ||||
| 	"presets": [ | ||||
| 		[ | ||||
| 			"@babel/preset-env", | ||||
| 			{ | ||||
| 				"targets": { | ||||
| 					"edge": "17", | ||||
| 					"firefox": "60", | ||||
| 					"chrome": "67", | ||||
| 					"safari": "11.1" | ||||
| 				} | ||||
| 			} | ||||
| 		] | ||||
| 	] | ||||
| } | ||||
							
								
								
									
										31
									
								
								biome.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								biome.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| { | ||||
| 	"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", | ||||
| 	"vcs": { | ||||
| 		"enabled": true, | ||||
| 		"clientKind": "git", | ||||
| 		"useIgnoreFile": true | ||||
| 	}, | ||||
| 	"files": { | ||||
| 		"ignoreUnknown": false, | ||||
| 		"ignore": ["core/static/vendored", "*.min.js", "staticfiles/generated"] | ||||
| 	}, | ||||
| 	"formatter": { | ||||
| 		"enabled": true, | ||||
| 		"indentStyle": "tab" | ||||
| 	}, | ||||
| 	"organizeImports": { | ||||
| 		"enabled": true | ||||
| 	}, | ||||
| 	"linter": { | ||||
| 		"enabled": true, | ||||
| 		"ignore": ["core/static/vendored", "*.min.js", "staticfiles/generated"], | ||||
| 		"rules": { | ||||
| 			"recommended": true | ||||
| 		} | ||||
| 	}, | ||||
| 	"javascript": { | ||||
| 		"formatter": { | ||||
| 			"quoteStyle": "double" | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,20 +1,23 @@ | ||||
| $(document).ready(function () { | ||||
|   $('#poster_list #view').click(function (e) { | ||||
|     $('#view').removeClass('active') | ||||
|   }) | ||||
| $(document).ready(() => { | ||||
| 	$("#poster_list #view").click((e) => { | ||||
| 		$("#view").removeClass("active"); | ||||
| 	}); | ||||
|  | ||||
|   $('#poster_list .poster .image').click(function (e) { | ||||
|     let el = $(e.target) | ||||
|     if (el.hasClass('image')) { el = el.find('img') } | ||||
|     $('#poster_list #view #placeholder').html(el.clone()) | ||||
| 	$("#poster_list .poster .image").click((e) => { | ||||
| 		let el = $(e.target); | ||||
| 		if (el.hasClass("image")) { | ||||
| 			el = el.find("img"); | ||||
| 		} | ||||
| 		$("#poster_list #view #placeholder").html(el.clone()); | ||||
|  | ||||
|     $('#view').addClass('active') | ||||
|   }) | ||||
| 		$("#view").addClass("active"); | ||||
| 	}); | ||||
|  | ||||
|   $(document).keyup(function (e) { | ||||
|     if (e.keyCode === 27) { // escape key maps to keycode `27` | ||||
|       e.preventDefault() | ||||
|       $('#view').removeClass('active') | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| 	$(document).keyup((e) => { | ||||
| 		if (e.keyCode === 27) { | ||||
| 			// escape key maps to keycode `27` | ||||
| 			e.preventDefault(); | ||||
| 			$("#view").removeClass("active"); | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
| @@ -1,104 +1,106 @@ | ||||
| // TODO: remove disable | ||||
| /* eslint-disable no-undef, camelcase */ | ||||
| $(document).ready(function () { | ||||
|   transition_time = 1000 | ||||
| $(document).ready(() => { | ||||
| 	transition_time = 1000; | ||||
|  | ||||
|   i = 0 | ||||
|   max = $('#slideshow .slide').length | ||||
| 	i = 0; | ||||
| 	max = $("#slideshow .slide").length; | ||||
|  | ||||
|   next_trigger = 0 | ||||
| 	next_trigger = 0; | ||||
|  | ||||
|   function enterFullscreen () { | ||||
|     element = document.getElementById('slideshow') | ||||
|     $(element).addClass('fullscreen') | ||||
|     if (element.requestFullscreen) { | ||||
|       element.requestFullscreen() | ||||
|     } else if (element.mozRequestFullScreen) { | ||||
|       element.mozRequestFullScreen() | ||||
|     } else if (element.webkitRequestFullscreen) { | ||||
|       element.webkitRequestFullscreen() | ||||
|     } else if (element.msRequestFullscreen) { | ||||
|       element.msRequestFullscreen() | ||||
|     } | ||||
|   } | ||||
| 	function enterFullscreen() { | ||||
| 		element = document.getElementById("slideshow"); | ||||
| 		$(element).addClass("fullscreen"); | ||||
| 		if (element.requestFullscreen) { | ||||
| 			element.requestFullscreen(); | ||||
| 		} else if (element.mozRequestFullScreen) { | ||||
| 			element.mozRequestFullScreen(); | ||||
| 		} else if (element.webkitRequestFullscreen) { | ||||
| 			element.webkitRequestFullscreen(); | ||||
| 		} else if (element.msRequestFullscreen) { | ||||
| 			element.msRequestFullscreen(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   function exitFullscreen () { | ||||
|     element = document.getElementById('slideshow') | ||||
|     $(element).removeClass('fullscreen') | ||||
|     if (document.exitFullscreen) { | ||||
|       document.exitFullscreen() | ||||
|     } else if (document.webkitExitFullscreen) { | ||||
|       document.webkitExitFullscreen() | ||||
|     } else if (document.mozCancelFullScreen) { | ||||
|       document.mozCancelFullScreen() | ||||
|     } else if (document.msExitFullscreen) { | ||||
|       document.msExitFullscreen() | ||||
|     } | ||||
|   } | ||||
| 	function exitFullscreen() { | ||||
| 		element = document.getElementById("slideshow"); | ||||
| 		$(element).removeClass("fullscreen"); | ||||
| 		if (document.exitFullscreen) { | ||||
| 			document.exitFullscreen(); | ||||
| 		} else if (document.webkitExitFullscreen) { | ||||
| 			document.webkitExitFullscreen(); | ||||
| 		} else if (document.mozCancelFullScreen) { | ||||
| 			document.mozCancelFullScreen(); | ||||
| 		} else if (document.msExitFullscreen) { | ||||
| 			document.msExitFullscreen(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   function init_progress_bar () { | ||||
|     $('#slideshow #progress_bar').css('transition', 'none') | ||||
|     $('#slideshow #progress_bar').removeClass('progress') | ||||
|     $('#slideshow #progress_bar').addClass('init') | ||||
|   } | ||||
| 	function init_progress_bar() { | ||||
| 		$("#slideshow #progress_bar").css("transition", "none"); | ||||
| 		$("#slideshow #progress_bar").removeClass("progress"); | ||||
| 		$("#slideshow #progress_bar").addClass("init"); | ||||
| 	} | ||||
|  | ||||
|   function start_progress_bar (display_time) { | ||||
|     $('#slideshow #progress_bar').removeClass('init') | ||||
|     $('#slideshow #progress_bar').addClass('progress') | ||||
|     $('#slideshow #progress_bar').css('transition', 'width ' + display_time + 's linear') | ||||
|   } | ||||
| 	function start_progress_bar(display_time) { | ||||
| 		$("#slideshow #progress_bar").removeClass("init"); | ||||
| 		$("#slideshow #progress_bar").addClass("progress"); | ||||
| 		$("#slideshow #progress_bar").css( | ||||
| 			"transition", | ||||
| 			`width ${display_time}s linear`, | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
|   function next () { | ||||
|     init_progress_bar() | ||||
|     slide = $($('#slideshow .slide').get(i % max)) | ||||
|     slide.removeClass('center') | ||||
|     slide.addClass('left') | ||||
| 	function next() { | ||||
| 		init_progress_bar(); | ||||
| 		slide = $($("#slideshow .slide").get(i % max)); | ||||
| 		slide.removeClass("center"); | ||||
| 		slide.addClass("left"); | ||||
|  | ||||
|     next_slide = $($('#slideshow .slide').get((i + 1) % max)) | ||||
|     next_slide.removeClass('right') | ||||
|     next_slide.addClass('center') | ||||
|     display_time = next_slide.attr('display_time') || 2 | ||||
| 		next_slide = $($("#slideshow .slide").get((i + 1) % max)); | ||||
| 		next_slide.removeClass("right"); | ||||
| 		next_slide.addClass("center"); | ||||
| 		display_time = next_slide.attr("display_time") || 2; | ||||
|  | ||||
|     $('#slideshow .bullet').removeClass('active') | ||||
|     bullet = $('#slideshow .bullet')[(i + 1) % max] | ||||
|     $(bullet).addClass('active') | ||||
| 		$("#slideshow .bullet").removeClass("active"); | ||||
| 		bullet = $("#slideshow .bullet")[(i + 1) % max]; | ||||
| 		$(bullet).addClass("active"); | ||||
|  | ||||
|     i = (i + 1) % max | ||||
| 		i = (i + 1) % max; | ||||
|  | ||||
|     setTimeout(function () { | ||||
|       others_left = $('#slideshow .slide.left') | ||||
|       others_left.removeClass('left') | ||||
|       others_left.addClass('right') | ||||
| 		setTimeout(() => { | ||||
| 			others_left = $("#slideshow .slide.left"); | ||||
| 			others_left.removeClass("left"); | ||||
| 			others_left.addClass("right"); | ||||
|  | ||||
|       start_progress_bar(display_time) | ||||
|       next_trigger = setTimeout(next, display_time * 1000) | ||||
|     }, transition_time) | ||||
|   } | ||||
| 			start_progress_bar(display_time); | ||||
| 			next_trigger = setTimeout(next, display_time * 1000); | ||||
| 		}, transition_time); | ||||
| 	} | ||||
|  | ||||
|   display_time = $('#slideshow .center').attr('display_time') | ||||
|   init_progress_bar() | ||||
|   setTimeout(function () { | ||||
|     if (max > 1) { | ||||
|       start_progress_bar(display_time) | ||||
|       setTimeout(next, display_time * 1000) | ||||
|     } | ||||
|   }, 10) | ||||
| 	display_time = $("#slideshow .center").attr("display_time"); | ||||
| 	init_progress_bar(); | ||||
| 	setTimeout(() => { | ||||
| 		if (max > 1) { | ||||
| 			start_progress_bar(display_time); | ||||
| 			setTimeout(next, display_time * 1000); | ||||
| 		} | ||||
| 	}, 10); | ||||
|  | ||||
|   $('#slideshow').click(function (e) { | ||||
|     if (!$('#slideshow').hasClass('fullscreen')) { | ||||
|       console.log('Entering fullscreen ...') | ||||
|       enterFullscreen() | ||||
|     } else { | ||||
|       console.log('Exiting fullscreen ...') | ||||
|       exitFullscreen() | ||||
|     } | ||||
|   }) | ||||
| 	$("#slideshow").click((e) => { | ||||
| 		if (!$("#slideshow").hasClass("fullscreen")) { | ||||
| 			console.log("Entering fullscreen ..."); | ||||
| 			enterFullscreen(); | ||||
| 		} else { | ||||
| 			console.log("Exiting fullscreen ..."); | ||||
| 			exitFullscreen(); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
|   $(document).keyup(function (e) { | ||||
|     if (e.keyCode === 27) { // escape key maps to keycode `27` | ||||
|       e.preventDefault() | ||||
|       console.log('Exiting fullscreen ...') | ||||
|       exitFullscreen() | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| 	$(document).keyup((e) => { | ||||
| 		if (e.keyCode === 27) { | ||||
| 			// escape key maps to keycode `27` | ||||
| 			e.preventDefault(); | ||||
| 			console.log("Exiting fullscreen ..."); | ||||
| 			exitFullscreen(); | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
| @@ -1 +1,14 @@ | ||||
| [{"pk": 1, "fields": {"permissions": [1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12, 19, 20, 21, 22, 25, 23, 24, 16, 17, 18, 13, 14, 15]}, "model": "core.group"}, {"pk": 2, "fields": {"permissions": [25]}, "model": "core.group"}, {"pk": 3, "fields": {"permissions": []}, "model": "core.group"}] | ||||
| [ | ||||
| 	{ | ||||
| 		"pk": 1, | ||||
| 		"fields": { | ||||
| 			"permissions": [ | ||||
| 				1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12, 19, 20, 21, 22, 25, 23, 24, 16, | ||||
| 				17, 18, 13, 14, 15 | ||||
| 			] | ||||
| 		}, | ||||
| 		"model": "core.group" | ||||
| 	}, | ||||
| 	{ "pk": 2, "fields": { "permissions": [25] }, "model": "core.group" }, | ||||
| 	{ "pk": 3, "fields": { "permissions": [] }, "model": "core.group" } | ||||
| ] | ||||
|   | ||||
| @@ -1 +1,74 @@ | ||||
| [{"pk": 1, "model": "core.page", "fields": {"full_name": "guy2", "owner_group": 1, "parent": null, "edit_groups": [], "name": "guy2", "view_groups": []}}, {"pk": 2, "model": "core.page", "fields": {"full_name": "guy2/bibou", "owner_group": 1, "parent": 1, "edit_group": [], "name": "bibou", "view_group": []}}, {"pk": 3, "model": "core.page", "fields": {"full_name": "guy2/bibou/troll", "owner_group": 1, "parent": 2, "edit_group": [], "name": "troll", "view_group": []}}, {"pk": 4, "model": "core.page", "fields": {"full_name": "guy", "owner_group": 1, "parent": null, "edit_group": [1], "name": "guy", "view_group": [1]}}, {"pk": 5, "model": "core.page", "fields": {"full_name": "bibou", "owner_group": 3, "parent": null, "edit_group": [1], "name": "bibou", "view_group": []}}, {"pk": 6, "model": "core.page", "fields": {"full_name": "guy2/guy", "owner_group": 1, "parent": 1, "edit_group": [], "name": "guy", "view_group": []}}] | ||||
| [ | ||||
| 	{ | ||||
| 		"pk": 1, | ||||
| 		"model": "core.page", | ||||
| 		"fields": { | ||||
| 			"full_name": "guy2", | ||||
| 			"owner_group": 1, | ||||
| 			"parent": null, | ||||
| 			"edit_groups": [], | ||||
| 			"name": "guy2", | ||||
| 			"view_groups": [] | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"pk": 2, | ||||
| 		"model": "core.page", | ||||
| 		"fields": { | ||||
| 			"full_name": "guy2/bibou", | ||||
| 			"owner_group": 1, | ||||
| 			"parent": 1, | ||||
| 			"edit_group": [], | ||||
| 			"name": "bibou", | ||||
| 			"view_group": [] | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"pk": 3, | ||||
| 		"model": "core.page", | ||||
| 		"fields": { | ||||
| 			"full_name": "guy2/bibou/troll", | ||||
| 			"owner_group": 1, | ||||
| 			"parent": 2, | ||||
| 			"edit_group": [], | ||||
| 			"name": "troll", | ||||
| 			"view_group": [] | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"pk": 4, | ||||
| 		"model": "core.page", | ||||
| 		"fields": { | ||||
| 			"full_name": "guy", | ||||
| 			"owner_group": 1, | ||||
| 			"parent": null, | ||||
| 			"edit_group": [1], | ||||
| 			"name": "guy", | ||||
| 			"view_group": [1] | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"pk": 5, | ||||
| 		"model": "core.page", | ||||
| 		"fields": { | ||||
| 			"full_name": "bibou", | ||||
| 			"owner_group": 3, | ||||
| 			"parent": null, | ||||
| 			"edit_group": [1], | ||||
| 			"name": "bibou", | ||||
| 			"view_group": [] | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"pk": 6, | ||||
| 		"model": "core.page", | ||||
| 		"fields": { | ||||
| 			"full_name": "guy2/guy", | ||||
| 			"owner_group": 1, | ||||
| 			"parent": 1, | ||||
| 			"edit_group": [], | ||||
| 			"name": "guy", | ||||
| 			"view_group": [] | ||||
| 		} | ||||
| 	} | ||||
| ] | ||||
|   | ||||
| @@ -1 +1,42 @@ | ||||
| [{"model": "core.user", "pk": 1, "fields": {"first_name": "Ro", "date_joined": "2015-11-19T16:05:51.764Z", "groups": [], "password": "pbkdf2_sha256$20000$MDukCN5X8Bof$rYdhppKiusj+W/1Rxpy0yuYsEyWocESEjtRsopkOc5c=", "last_name": "Ot", "nick_name": "", "username": "root", "user_permissions": [], "email": "bibou@git.an", "last_login": "2015-11-26T16:28:36.464Z", "date_of_birth": "1969-12-31T23:00:00Z", "is_superuser": true, "is_active": true, "is_staff": true}}, {"model": "core.user", "pk": 2, "fields": {"first_name": "Skia", "date_joined": "2015-11-19T16:06:29.556Z", "groups": [3], "password": "pbkdf2_sha256$20000$UK9a29p5bBEh$Jzv7xs0W9njJZiXfIdYXDydim/3YHs6awKwDmN7gSAc=", "last_name": "Kia", "nick_name": "", "username": "skia", "user_permissions": [], "email": "plop@libskia.so", "last_login": "2015-11-26T16:37:01.671Z", "date_of_birth": "1969-12-31T23:00:00Z", "is_superuser": false, "is_active": true, "is_staff": false}}] | ||||
| [ | ||||
| 	{ | ||||
| 		"model": "core.user", | ||||
| 		"pk": 1, | ||||
| 		"fields": { | ||||
| 			"first_name": "Ro", | ||||
| 			"date_joined": "2015-11-19T16:05:51.764Z", | ||||
| 			"groups": [], | ||||
| 			"password": "pbkdf2_sha256$20000$MDukCN5X8Bof$rYdhppKiusj+W/1Rxpy0yuYsEyWocESEjtRsopkOc5c=", | ||||
| 			"last_name": "Ot", | ||||
| 			"nick_name": "", | ||||
| 			"username": "root", | ||||
| 			"user_permissions": [], | ||||
| 			"email": "bibou@git.an", | ||||
| 			"last_login": "2015-11-26T16:28:36.464Z", | ||||
| 			"date_of_birth": "1969-12-31T23:00:00Z", | ||||
| 			"is_superuser": true, | ||||
| 			"is_active": true, | ||||
| 			"is_staff": true | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "core.user", | ||||
| 		"pk": 2, | ||||
| 		"fields": { | ||||
| 			"first_name": "Skia", | ||||
| 			"date_joined": "2015-11-19T16:06:29.556Z", | ||||
| 			"groups": [3], | ||||
| 			"password": "pbkdf2_sha256$20000$UK9a29p5bBEh$Jzv7xs0W9njJZiXfIdYXDydim/3YHs6awKwDmN7gSAc=", | ||||
| 			"last_name": "Kia", | ||||
| 			"nick_name": "", | ||||
| 			"username": "skia", | ||||
| 			"user_permissions": [], | ||||
| 			"email": "plop@libskia.so", | ||||
| 			"last_login": "2015-11-26T16:37:01.671Z", | ||||
| 			"date_of_birth": "1969-12-31T23:00:00Z", | ||||
| 			"is_superuser": false, | ||||
| 			"is_active": true, | ||||
| 			"is_staff": false | ||||
| 		} | ||||
| 	} | ||||
| ] | ||||
|   | ||||
| @@ -1,42 +1,121 @@ | ||||
| /*--------------------------------RESET--------------------------------*/ | ||||
| /*--------------------------------RESET--------------------------------*/ | ||||
| html, body, div, span, applet, object, iframe, | ||||
| h1, h2, h3, h4, h5, h6, p, blockquote, pre, | ||||
| a, abbr, acronym, address, big, cite, code, | ||||
| del, dfn, em, img, ins, kbd, q, s, samp, | ||||
| small, strike, sub, sup, tt, var, | ||||
| b, u, i, center, | ||||
| dl, dt, dd, ol, ul, li, | ||||
| fieldset, form, label, legend, | ||||
| table, caption, tbody, tfoot, thead, tr, th, td, | ||||
| article, aside, canvas, details, embed, | ||||
| figure, figcaption, footer, header, hgroup, | ||||
| menu, nav, output, ruby, section, summary, | ||||
| time, mark, audio, video { | ||||
| html, | ||||
| body, | ||||
| div, | ||||
| span, | ||||
| applet, | ||||
| object, | ||||
| iframe, | ||||
| h1, | ||||
| h2, | ||||
| h3, | ||||
| h4, | ||||
| h5, | ||||
| h6, | ||||
| p, | ||||
| blockquote, | ||||
| pre, | ||||
| a, | ||||
| abbr, | ||||
| acronym, | ||||
| address, | ||||
| big, | ||||
| cite, | ||||
| code, | ||||
| del, | ||||
| dfn, | ||||
| em, | ||||
| img, | ||||
| ins, | ||||
| kbd, | ||||
| q, | ||||
| s, | ||||
| samp, | ||||
| small, | ||||
| strike, | ||||
| sub, | ||||
| sup, | ||||
| tt, | ||||
| var, | ||||
| b, | ||||
| u, | ||||
| i, | ||||
| center, | ||||
| dl, | ||||
| dt, | ||||
| dd, | ||||
| ol, | ||||
| ul, | ||||
| li, | ||||
| fieldset, | ||||
| form, | ||||
| label, | ||||
| legend, | ||||
| table, | ||||
| caption, | ||||
| tbody, | ||||
| tfoot, | ||||
| thead, | ||||
| tr, | ||||
| th, | ||||
| td, | ||||
| article, | ||||
| aside, | ||||
| canvas, | ||||
| details, | ||||
| embed, | ||||
| figure, | ||||
| figcaption, | ||||
| footer, | ||||
| header, | ||||
| hgroup, | ||||
| menu, | ||||
| nav, | ||||
| output, | ||||
| ruby, | ||||
| section, | ||||
| summary, | ||||
| time, | ||||
| mark, | ||||
| audio, | ||||
| video { | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
| 	border: 0; | ||||
| 	font-size: 100%; | ||||
| 	font: inherit; | ||||
| 	vertical-align: baseline; | ||||
| } | ||||
| /* HTML5 display-role reset for older browsers */ | ||||
| article, aside, details, figcaption, figure, | ||||
| footer, header, hgroup, menu, nav, section { | ||||
| article, | ||||
| aside, | ||||
| details, | ||||
| figcaption, | ||||
| figure, | ||||
| footer, | ||||
| header, | ||||
| hgroup, | ||||
| menu, | ||||
| nav, | ||||
| section { | ||||
| 	display: block; | ||||
| } | ||||
| body { | ||||
| 	line-height: 1; | ||||
| } | ||||
| ol, ul { | ||||
| ol, | ||||
| ul { | ||||
| 	/* list-style: none;*/ | ||||
| } | ||||
| blockquote, q { | ||||
| blockquote, | ||||
| q { | ||||
| 	quotes: none; | ||||
| } | ||||
| blockquote:before, blockquote:after, | ||||
| q:before, q:after { | ||||
| 	content: ''; | ||||
| blockquote:before, | ||||
| blockquote:after, | ||||
| q:before, | ||||
| q:after { | ||||
| 	content: ""; | ||||
| 	content: none; | ||||
| } | ||||
| table { | ||||
|   | ||||
| @@ -1,60 +1,66 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| $(function () { | ||||
|   // const buttons = $('.choose_file_button') | ||||
|   const popups = $('.choose_file_widget') | ||||
|   popups.dialog({ | ||||
|     autoOpen: false, | ||||
|     modal: true, | ||||
|     width: '90%', | ||||
|     create: function (event) { | ||||
|       const target = $(event.target) | ||||
|       target.parent().css({ | ||||
|         position: 'fixed', | ||||
|         top: '5%', | ||||
|         bottom: '5%' | ||||
|       }) | ||||
|       target.css('height', '300px') | ||||
|       console.log(target) | ||||
|     }, | ||||
|     buttons: [ | ||||
|       { | ||||
|         text: 'Choose', | ||||
|         click: function () { | ||||
|           console.log($('#file_id')) | ||||
|           $('input[name=' + $(this).attr('name') + ']').attr('value', $('#file_id').attr('value')) | ||||
|           $(this).dialog('close') | ||||
|         }, | ||||
|         disabled: true | ||||
|       } | ||||
|     ] | ||||
|   }) | ||||
|   $('.choose_file_button').button().on('click', function () { | ||||
|     const popup = popups.filter('[name=' + $(this).attr('name') + ']') | ||||
|     console.log(popup) | ||||
|     popup.html('<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />') | ||||
|     popup.dialog({ title: $(this).text() }).dialog('open') | ||||
|   }) | ||||
|   $('#quick_notif li').click(function () { | ||||
|     $(this).hide() | ||||
|   }) | ||||
| }) | ||||
| $(() => { | ||||
| 	// const buttons = $('.choose_file_button') | ||||
| 	const popups = $(".choose_file_widget"); | ||||
| 	popups.dialog({ | ||||
| 		autoOpen: false, | ||||
| 		modal: true, | ||||
| 		width: "90%", | ||||
| 		create: (event) => { | ||||
| 			const target = $(event.target); | ||||
| 			target.parent().css({ | ||||
| 				position: "fixed", | ||||
| 				top: "5%", | ||||
| 				bottom: "5%", | ||||
| 			}); | ||||
| 			target.css("height", "300px"); | ||||
| 			console.log(target); | ||||
| 		}, | ||||
| 		buttons: [ | ||||
| 			{ | ||||
| 				text: "Choose", | ||||
| 				click: function () { | ||||
| 					console.log($("#file_id")); | ||||
| 					$(`input[name=${$(this).attr("name")}]`).attr( | ||||
| 						"value", | ||||
| 						$("#file_id").attr("value"), | ||||
| 					); | ||||
| 					$(this).dialog("close"); | ||||
| 				}, | ||||
| 				disabled: true, | ||||
| 			}, | ||||
| 		], | ||||
| 	}); | ||||
| 	$(".choose_file_button") | ||||
| 		.button() | ||||
| 		.on("click", function () { | ||||
| 			const popup = popups.filter(`[name=${$(this).attr("name")}]`); | ||||
| 			console.log(popup); | ||||
| 			popup.html( | ||||
| 				'<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />', | ||||
| 			); | ||||
| 			popup.dialog({ title: $(this).text() }).dialog("open"); | ||||
| 		}); | ||||
| 	$("#quick_notif li").click(function () { | ||||
| 		$(this).hide(); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| export function createQuickNotif (msg) { | ||||
|   const el = document.createElement('li') | ||||
|   el.textContent = msg | ||||
|   el.addEventListener('click', () => el.parentNode.removeChild(el)) | ||||
|   document.getElementById('quick_notif').appendChild(el) | ||||
| function createQuickNotif(msg) { | ||||
| 	const el = document.createElement("li"); | ||||
| 	el.textContent = msg; | ||||
| 	el.addEventListener("click", () => el.parentNode.removeChild(el)); | ||||
| 	document.getElementById("quick_notif").appendChild(el); | ||||
| } | ||||
|  | ||||
| export function deleteQuickNotifs () { | ||||
|   const el = document.getElementById('quick_notif') | ||||
|   while (el.firstChild) { | ||||
|     el.removeChild(el.firstChild) | ||||
|   } | ||||
| function deleteQuickNotifs() { | ||||
| 	const el = document.getElementById("quick_notif"); | ||||
| 	while (el.firstChild) { | ||||
| 		el.removeChild(el.firstChild); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export function display_notif () { | ||||
|   $('#header_notif').toggle().parent().toggleClass('white') | ||||
| function display_notif() { | ||||
| 	$("#header_notif").toggle().parent().toggleClass("white"); | ||||
| } | ||||
|  | ||||
| // You can't get the csrf token from the template in a widget | ||||
| @@ -63,21 +69,21 @@ export function display_notif () { | ||||
| // Sadly, getting the cookie is not possible with CSRF_COOKIE_HTTPONLY or CSRF_USE_SESSIONS is True | ||||
| // So, the true workaround is to get the token from the dom | ||||
| // https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true | ||||
| export function getCSRFToken () { | ||||
|   return $('[name=csrfmiddlewaretoken]').val() | ||||
| function getCSRFToken() { | ||||
| 	return $("[name=csrfmiddlewaretoken]").val(); | ||||
| } | ||||
|  | ||||
| export const initialUrlParams = new URLSearchParams(window.location.search) | ||||
| const initialUrlParams = new URLSearchParams(window.location.search); | ||||
|  | ||||
| /** | ||||
|  * @readonly | ||||
|  * @enum {number} | ||||
|  */ | ||||
| const History = { | ||||
|   NONE: 0, | ||||
|   PUSH: 1, | ||||
|   REPLACE: 2 | ||||
| } | ||||
| 	NONE: 0, | ||||
| 	PUSH: 1, | ||||
| 	REPLACE: 2, | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param {string} key | ||||
| @@ -85,27 +91,30 @@ const History = { | ||||
|  * @param {History} action | ||||
|  * @param {URL | null} url | ||||
|  */ | ||||
| export function update_query_string (key, value, action = History.REPLACE, url = null) { | ||||
|   if (!url) { | ||||
|     url = new URL(window.location.href) | ||||
|   } | ||||
|   if (value === undefined || value === null || value === '') { | ||||
|     // If the value is null, undefined or empty => delete it | ||||
|     url.searchParams.delete(key) | ||||
|   } else if (Array.isArray(value)) { | ||||
|     url.searchParams.delete(key) | ||||
|     value.forEach((v) => url.searchParams.append(key, v)) | ||||
|   } else { | ||||
|     url.searchParams.set(key, value) | ||||
|   } | ||||
| function update_query_string(key, value, action = History.REPLACE, url = null) { | ||||
| 	let ret = url; | ||||
| 	if (!ret) { | ||||
| 		ret = new URL(window.location.href); | ||||
| 	} | ||||
| 	if (value === undefined || value === null || value === "") { | ||||
| 		// If the value is null, undefined or empty => delete it | ||||
| 		ret.searchParams.delete(key); | ||||
| 	} else if (Array.isArray(value)) { | ||||
| 		ret.searchParams.delete(key); | ||||
| 		for (const v of value) { | ||||
| 			ret.searchParams.append(key, v); | ||||
| 		} | ||||
| 	} else { | ||||
| 		ret.searchParams.set(key, value); | ||||
| 	} | ||||
|  | ||||
|   if (action === History.PUSH) { | ||||
|     window.history.pushState(null, '', url.toString()) | ||||
|   } else if (action === History.REPLACE) { | ||||
|     window.history.replaceState(null, '', url.toString()) | ||||
|   } | ||||
| 	if (action === History.PUSH) { | ||||
| 		window.history.pushState(null, "", ret.toString()); | ||||
| 	} else if (action === History.REPLACE) { | ||||
| 		window.history.replaceState(null, "", ret.toString()); | ||||
| 	} | ||||
|  | ||||
|   return url | ||||
| 	return ret; | ||||
| } | ||||
|  | ||||
| // TODO : If one day a test workflow is made for JS in this project | ||||
| @@ -116,27 +125,29 @@ export function update_query_string (key, value, action = History.REPLACE, url = | ||||
|  * @param {string} url The paginated endpoint to fetch | ||||
|  * @return {Promise<Object[]>} | ||||
|  */ | ||||
| export async function fetch_paginated (url) { | ||||
|   const max_per_page = 199 | ||||
|   const paginated_url = new URL(url, document.location.origin) | ||||
|   paginated_url.searchParams.set('page_size', max_per_page.toString()) | ||||
|   paginated_url.searchParams.set('page', '1') | ||||
| async function fetch_paginated(url) { | ||||
| 	const max_per_page = 199; | ||||
| 	const paginated_url = new URL(url, document.location.origin); | ||||
| 	paginated_url.searchParams.set("page_size", max_per_page.toString()); | ||||
| 	paginated_url.searchParams.set("page", "1"); | ||||
|  | ||||
|   const first_page = (await (await fetch(paginated_url)).json()) | ||||
|   const results = first_page.results | ||||
| 	const first_page = await (await fetch(paginated_url)).json(); | ||||
| 	const results = first_page.results; | ||||
|  | ||||
|   const nb_pictures = first_page.count | ||||
|   const nb_pages = Math.ceil(nb_pictures / max_per_page) | ||||
| 	const nb_pictures = first_page.count; | ||||
| 	const nb_pages = Math.ceil(nb_pictures / max_per_page); | ||||
|  | ||||
|   if (nb_pages > 1) { | ||||
|     const promises = [] | ||||
|     for (let i = 2; i <= nb_pages; i++) { | ||||
|       paginated_url.searchParams.set('page', i.toString()) | ||||
|       promises.push( | ||||
|         fetch(paginated_url).then(res => res.json().then(json => json.results)) | ||||
|       ) | ||||
|     } | ||||
|     results.push(...(await Promise.all(promises)).flat()) | ||||
|   } | ||||
|   return results | ||||
| 	if (nb_pages > 1) { | ||||
| 		const promises = []; | ||||
| 		for (let i = 2; i <= nb_pages; i++) { | ||||
| 			paginated_url.searchParams.set("page", i.toString()); | ||||
| 			promises.push( | ||||
| 				fetch(paginated_url).then((res) => | ||||
| 					res.json().then((json) => json.results), | ||||
| 				), | ||||
| 			); | ||||
| 		} | ||||
| 		results.push(...(await Promise.all(promises)).flat()); | ||||
| 	} | ||||
| 	return results; | ||||
| } | ||||
|   | ||||
							
								
								
									
										104
									
								
								core/static/core/js/shorten.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										104
									
								
								core/static/core/js/shorten.min.js
									
									
									
									
										vendored
									
									
								
							| @@ -19,4 +19,106 @@ | ||||
| // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||
| // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||
| // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
| !function(e){e.fn.shorten=function(s){"use strict";var t={showChars:100,minHideChars:10,ellipsesText:"...",moreText:"more",lessText:"less",onLess:function(){},onMore:function(){},errMsg:null,force:!1};return s&&e.extend(t,s),(!e(this).data("jquery.shorten")||!!t.force)&&(e(this).data("jquery.shorten",!0),e(document).off("click",".morelink"),e(document).on({click:function(){var s=e(this);return s.hasClass("less")?(s.removeClass("less"),s.html(t.moreText),s.parent().prev().animate({},function(){s.parent().prev().prev().show()}).hide("fast",function(){t.onLess()})):(s.addClass("less"),s.html(t.lessText),s.parent().prev().animate({},function(){s.parent().prev().prev().hide()}).show("fast",function(){t.onMore()})),!1}},".morelink"),this.each(function(){var s=e(this),n=s.html();if(s.text().length>t.showChars+t.minHideChars){var r=n.substr(0,t.showChars);if(r.indexOf("<")>=0){for(var a=!1,o="",i=0,l=[],h=null,c=0,f=0;f<=t.showChars;c++)if("<"!=n[c]||a||(a=!0,"/"==(h=n.substring(c+1,n.indexOf(">",c)))[0]?h!="/"+l[0]?t.errMsg="ERROR en HTML: the top of the stack should be the tag that closes":l.shift():"br"!=h.toLowerCase()&&l.unshift(h)),a&&">"==n[c]&&(a=!1),a)o+=n.charAt(c);else if(f++,i<=t.showChars)o+=n.charAt(c),i++;else if(l.length>0){for(j=0;j<l.length;j++)o+="</"+l[j]+">";break}r=e("<div/>").html(o+'<span class="ellip">'+t.ellipsesText+"</span>").html()}else r+=t.ellipsesText;var p='<div class="shortcontent">'+r+'</div><div class="allcontent">'+n+'</div><span><a href="javascript://nop/" class="morelink">'+t.moreText+"</a></span>";s.html(p),s.find(".allcontent").hide(),e(".shortcontent p:last",s).css("margin-bottom",0)}}))}}(jQuery); | ||||
| !(function (e) { | ||||
| 	e.fn.shorten = function (s) { | ||||
| 		"use strict"; | ||||
| 		var t = { | ||||
| 			showChars: 100, | ||||
| 			minHideChars: 10, | ||||
| 			ellipsesText: "...", | ||||
| 			moreText: "more", | ||||
| 			lessText: "less", | ||||
| 			onLess: function () {}, | ||||
| 			onMore: function () {}, | ||||
| 			errMsg: null, | ||||
| 			force: !1, | ||||
| 		}; | ||||
| 		return ( | ||||
| 			s && e.extend(t, s), | ||||
| 			(!e(this).data("jquery.shorten") || !!t.force) && | ||||
| 				(e(this).data("jquery.shorten", !0), | ||||
| 				e(document).off("click", ".morelink"), | ||||
| 				e(document).on( | ||||
| 					{ | ||||
| 						click: function () { | ||||
| 							var s = e(this); | ||||
| 							return ( | ||||
| 								s.hasClass("less") | ||||
| 									? (s.removeClass("less"), | ||||
| 										s.html(t.moreText), | ||||
| 										s | ||||
| 											.parent() | ||||
| 											.prev() | ||||
| 											.animate({}, function () { | ||||
| 												s.parent().prev().prev().show(); | ||||
| 											}) | ||||
| 											.hide("fast", function () { | ||||
| 												t.onLess(); | ||||
| 											})) | ||||
| 									: (s.addClass("less"), | ||||
| 										s.html(t.lessText), | ||||
| 										s | ||||
| 											.parent() | ||||
| 											.prev() | ||||
| 											.animate({}, function () { | ||||
| 												s.parent().prev().prev().hide(); | ||||
| 											}) | ||||
| 											.show("fast", function () { | ||||
| 												t.onMore(); | ||||
| 											})), | ||||
| 								!1 | ||||
| 							); | ||||
| 						}, | ||||
| 					}, | ||||
| 					".morelink", | ||||
| 				), | ||||
| 				this.each(function () { | ||||
| 					var s = e(this), | ||||
| 						n = s.html(); | ||||
| 					if (s.text().length > t.showChars + t.minHideChars) { | ||||
| 						var r = n.substr(0, t.showChars); | ||||
| 						if (r.indexOf("<") >= 0) { | ||||
| 							for ( | ||||
| 								var a = !1, o = "", i = 0, l = [], h = null, c = 0, f = 0; | ||||
| 								f <= t.showChars; | ||||
| 								c++ | ||||
| 							) | ||||
| 								if ( | ||||
| 									("<" != n[c] || | ||||
| 										a || | ||||
| 										((a = !0), | ||||
| 										"/" == (h = n.substring(c + 1, n.indexOf(">", c)))[0] | ||||
| 											? h != "/" + l[0] | ||||
| 												? (t.errMsg = | ||||
| 														"ERROR en HTML: the top of the stack should be the tag that closes") | ||||
| 												: l.shift() | ||||
| 											: "br" != h.toLowerCase() && l.unshift(h)), | ||||
| 									a && ">" == n[c] && (a = !1), | ||||
| 									a) | ||||
| 								) | ||||
| 									o += n.charAt(c); | ||||
| 								else if ((f++, i <= t.showChars)) (o += n.charAt(c)), i++; | ||||
| 								else if (l.length > 0) { | ||||
| 									for (j = 0; j < l.length; j++) o += "</" + l[j] + ">"; | ||||
| 									break; | ||||
| 								} | ||||
| 							r = e("<div/>") | ||||
| 								.html(o + '<span class="ellip">' + t.ellipsesText + "</span>") | ||||
| 								.html(); | ||||
| 						} else r += t.ellipsesText; | ||||
| 						var p = | ||||
| 							'<div class="shortcontent">' + | ||||
| 							r + | ||||
| 							'</div><div class="allcontent">' + | ||||
| 							n + | ||||
| 							'</div><span><a href="javascript://nop/" class="morelink">' + | ||||
| 							t.moreText + | ||||
| 							"</a></span>"; | ||||
| 						s.html(p), | ||||
| 							s.find(".allcontent").hide(), | ||||
| 							e(".shortcontent p:last", s).css("margin-bottom", 0); | ||||
| 					} | ||||
| 				})) | ||||
| 		); | ||||
| 	}; | ||||
| })(jQuery); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| /** | ||||
|  * Builders to use Select2 in our templates. | ||||
|  * | ||||
| @@ -158,15 +157,15 @@ | ||||
| /** | ||||
|  * @param {Select2Options} options | ||||
|  */ | ||||
| export function sithSelect2 (options) { | ||||
|   const elem = $(options.element) | ||||
|   return elem.select2({ | ||||
|     theme: elem[0].multiple ? 'classic' : 'default', | ||||
|     minimumInputLength: 2, | ||||
|     templateResult: select_item_builder(options.picture_getter), | ||||
|     ...options.data_source, | ||||
|     ...(options.overrides || {}) | ||||
|   }) | ||||
| function sithSelect2(options) { | ||||
| 	const elem = $(options.element); | ||||
| 	return elem.select2({ | ||||
| 		theme: elem[0].multiple ? "classic" : "default", | ||||
| 		minimumInputLength: 2, | ||||
| 		templateResult: select_item_builder(options.picture_getter), | ||||
| 		...options.data_source, | ||||
| 		...(options.overrides || {}), | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -180,12 +179,12 @@ export function sithSelect2 (options) { | ||||
|  * @param {Select2Object[]} source The array containing the data | ||||
|  * @param {RemoteSourceOptions} options | ||||
|  */ | ||||
| export function local_data_source (source, options) { | ||||
|   if (options.excluded) { | ||||
|     const ids = options.excluded() | ||||
|     return { data: source.filter((i) => !ids.includes(i.id)) } | ||||
|   } | ||||
|   return { data: source } | ||||
| function local_data_source(source, options) { | ||||
| 	if (options.excluded) { | ||||
| 		const ids = options.excluded(); | ||||
| 		return { data: source.filter((i) => !ids.includes(i.id)) }; | ||||
| 	} | ||||
| 	return { data: source }; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -203,38 +202,38 @@ export function local_data_source (source, options) { | ||||
|  * @param {string} source The url of the endpoint | ||||
|  * @param {RemoteSourceOptions} options | ||||
|  */ | ||||
| export function remote_data_source (source, options) { | ||||
|   jQuery.ajaxSettings.traditional = true | ||||
|   const params = { | ||||
|     url: source, | ||||
|     dataType: 'json', | ||||
|     cache: true, | ||||
|     delay: 250, | ||||
|     data: function (params) { | ||||
|       return { | ||||
|         search: params.term, | ||||
|         exclude: [ | ||||
|           ...(this.val() || []).map((i) => parseInt(i)), | ||||
|           ...(options.excluded ? options.excluded() : []) | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (options.result_converter) { | ||||
|     params.processResults = function (data) { | ||||
|       return { results: data.results.map(options.result_converter) } | ||||
|     } | ||||
|   } | ||||
|   if (options.overrides) { | ||||
|     Object.assign(params, options.overrides) | ||||
|   } | ||||
|   return { ajax: params } | ||||
| function remote_data_source(source, options) { | ||||
| 	jQuery.ajaxSettings.traditional = true; | ||||
| 	const params = { | ||||
| 		url: source, | ||||
| 		dataType: "json", | ||||
| 		cache: true, | ||||
| 		delay: 250, | ||||
| 		data: function (params) { | ||||
| 			return { | ||||
| 				search: params.term, | ||||
| 				exclude: [ | ||||
| 					...(this.val() || []).map((i) => Number.parseInt(i)), | ||||
| 					...(options.excluded ? options.excluded() : []), | ||||
| 				], | ||||
| 			}; | ||||
| 		}, | ||||
| 	}; | ||||
| 	if (options.result_converter) { | ||||
| 		params.processResults = (data) => ({ | ||||
| 			results: data.results.map(options.result_converter), | ||||
| 		}); | ||||
| 	} | ||||
| 	if (options.overrides) { | ||||
| 		Object.assign(params, options.overrides); | ||||
| 	} | ||||
| 	return { ajax: params }; | ||||
| } | ||||
|  | ||||
| export function item_formatter (user) { | ||||
|   if (user.loading) { | ||||
|     return user.text | ||||
|   } | ||||
| function item_formatter(user) { | ||||
| 	if (user.loading) { | ||||
| 		return user.text; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -242,21 +241,21 @@ export function item_formatter (user) { | ||||
|  * @param {null | function(Object):string} picture_getter | ||||
|  * @return {function(string): jQuery|HTMLElement} | ||||
|  */ | ||||
| function select_item_builder (picture_getter) { | ||||
|   return (item) => { | ||||
|     const picture = | ||||
|       typeof picture_getter === 'function' ? picture_getter(item) : null | ||||
|     const img_html = picture | ||||
|       ? `<img  | ||||
| function select_item_builder(picture_getter) { | ||||
| 	return (item) => { | ||||
| 		const picture = | ||||
| 			typeof picture_getter === "function" ? picture_getter(item) : null; | ||||
| 		const img_html = picture | ||||
| 			? `<img  | ||||
|           src="${picture_getter(item)}"  | ||||
|           alt="${item.text}"  | ||||
|           onerror="this.src = '/static/core/img/unknown.jpg'"  | ||||
|         />` | ||||
|       : '' | ||||
| 			: ""; | ||||
|  | ||||
|     return $(`<div class="select-item"> | ||||
| 		return $(`<div class="select-item"> | ||||
|         ${img_html} | ||||
|          <span class="select-item-text">${item.text}</span> | ||||
|          </div>`) | ||||
|   } | ||||
|          </div>`); | ||||
| 	}; | ||||
| } | ||||
|   | ||||
| @@ -1,172 +1,167 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| /* global cytoscape, initialUrlParams, History, update_query_string */ | ||||
|  | ||||
| async function get_graph_data (url, godfathers_depth, godchildren_depth) { | ||||
|   const data = await ( | ||||
|     await fetch( | ||||
|       `${url}?godfathers_depth=${godfathers_depth}&godchildren_depth=${godchildren_depth}` | ||||
|     ) | ||||
|   ).json() | ||||
|   return [ | ||||
|     ...data.users.map((user) => { | ||||
|       return { data: user } | ||||
|     }), | ||||
|     ...data.relationships.map((rel) => { | ||||
|       return { | ||||
|         data: { source: rel.godfather, target: rel.godchild } | ||||
|       } | ||||
|     }) | ||||
|   ] | ||||
| async function get_graph_data(url, godfathers_depth, godchildren_depth) { | ||||
| 	const data = await ( | ||||
| 		await fetch( | ||||
| 			`${url}?godfathers_depth=${godfathers_depth}&godchildren_depth=${godchildren_depth}`, | ||||
| 		) | ||||
| 	).json(); | ||||
| 	return [ | ||||
| 		...data.users.map((user) => { | ||||
| 			return { data: user }; | ||||
| 		}), | ||||
| 		...data.relationships.map((rel) => { | ||||
| 			return { | ||||
| 				data: { source: rel.godfather, target: rel.godchild }, | ||||
| 			}; | ||||
| 		}), | ||||
| 	]; | ||||
| } | ||||
|  | ||||
| function create_graph (container, data, active_user_id) { | ||||
|   const cy = cytoscape({ | ||||
|     boxSelectionEnabled: false, | ||||
|     autounselectify: true, | ||||
| function create_graph(container, data, active_user_id) { | ||||
| 	const cy = cytoscape({ | ||||
| 		boxSelectionEnabled: false, | ||||
| 		autounselectify: true, | ||||
|  | ||||
|     container, | ||||
|     elements: data, | ||||
|     minZoom: 0.5, | ||||
| 		container, | ||||
| 		elements: data, | ||||
| 		minZoom: 0.5, | ||||
|  | ||||
|     style: [ | ||||
|       // the stylesheet for the graph | ||||
|       { | ||||
|         selector: 'node', | ||||
|         style: { | ||||
|           label: 'data(display_name)', | ||||
|           'background-image': 'data(profile_pict)', | ||||
|           width: '100%', | ||||
|           height: '100%', | ||||
|           'background-fit': 'cover', | ||||
|           'background-repeat': 'no-repeat', | ||||
|           shape: 'ellipse' | ||||
|         } | ||||
|       }, | ||||
| 		style: [ | ||||
| 			// the stylesheet for the graph | ||||
| 			{ | ||||
| 				selector: "node", | ||||
| 				style: { | ||||
| 					label: "data(display_name)", | ||||
| 					"background-image": "data(profile_pict)", | ||||
| 					width: "100%", | ||||
| 					height: "100%", | ||||
| 					"background-fit": "cover", | ||||
| 					"background-repeat": "no-repeat", | ||||
| 					shape: "ellipse", | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
|       { | ||||
|         selector: 'edge', | ||||
|         style: { | ||||
|           width: 5, | ||||
|           'line-color': '#ccc', | ||||
|           'target-arrow-color': '#ccc', | ||||
|           'target-arrow-shape': 'triangle', | ||||
|           'curve-style': 'bezier' | ||||
|         } | ||||
|       }, | ||||
| 			{ | ||||
| 				selector: "edge", | ||||
| 				style: { | ||||
| 					width: 5, | ||||
| 					"line-color": "#ccc", | ||||
| 					"target-arrow-color": "#ccc", | ||||
| 					"target-arrow-shape": "triangle", | ||||
| 					"curve-style": "bezier", | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
|       { | ||||
|         selector: '.traversed', | ||||
|         style: { | ||||
|           'border-width': '5px', | ||||
|           'border-style': 'solid', | ||||
|           'border-color': 'red', | ||||
|           'target-arrow-color': 'red', | ||||
|           'line-color': 'red' | ||||
|         } | ||||
|       }, | ||||
| 			{ | ||||
| 				selector: ".traversed", | ||||
| 				style: { | ||||
| 					"border-width": "5px", | ||||
| 					"border-style": "solid", | ||||
| 					"border-color": "red", | ||||
| 					"target-arrow-color": "red", | ||||
| 					"line-color": "red", | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
|       { | ||||
|         selector: '.not-traversed', | ||||
|         style: { | ||||
|           'line-opacity': '0.5', | ||||
|           'background-opacity': '0.5', | ||||
|           'background-image-opacity': '0.5' | ||||
|         } | ||||
|       } | ||||
|     ], | ||||
|     layout: { | ||||
|       name: 'klay', | ||||
|       nodeDimensionsIncludeLabels: true, | ||||
|       fit: true, | ||||
|       klay: { | ||||
|         addUnnecessaryBendpoints: true, | ||||
|         direction: 'DOWN', | ||||
|         nodePlacement: 'INTERACTIVE', | ||||
|         layoutHierarchy: true | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|   const active_user = cy | ||||
|     .getElementById(active_user_id) | ||||
|     .style('shape', 'rectangle') | ||||
|   /* Reset graph */ | ||||
|   const reset_graph = () => { | ||||
|     cy.elements((element) => { | ||||
|       if (element.hasClass('traversed')) { | ||||
|         element.removeClass('traversed') | ||||
|       } | ||||
|       if (element.hasClass('not-traversed')) { | ||||
|         element.removeClass('not-traversed') | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 			{ | ||||
| 				selector: ".not-traversed", | ||||
| 				style: { | ||||
| 					"line-opacity": "0.5", | ||||
| 					"background-opacity": "0.5", | ||||
| 					"background-image-opacity": "0.5", | ||||
| 				}, | ||||
| 			}, | ||||
| 		], | ||||
| 		layout: { | ||||
| 			name: "klay", | ||||
| 			nodeDimensionsIncludeLabels: true, | ||||
| 			fit: true, | ||||
| 			klay: { | ||||
| 				addUnnecessaryBendpoints: true, | ||||
| 				direction: "DOWN", | ||||
| 				nodePlacement: "INTERACTIVE", | ||||
| 				layoutHierarchy: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}); | ||||
| 	const active_user = cy | ||||
| 		.getElementById(active_user_id) | ||||
| 		.style("shape", "rectangle"); | ||||
| 	/* Reset graph */ | ||||
| 	const reset_graph = () => { | ||||
| 		cy.elements((element) => { | ||||
| 			if (element.hasClass("traversed")) { | ||||
| 				element.removeClass("traversed"); | ||||
| 			} | ||||
| 			if (element.hasClass("not-traversed")) { | ||||
| 				element.removeClass("not-traversed"); | ||||
| 			} | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
|   const on_node_tap = (el) => { | ||||
|     reset_graph() | ||||
|     /* Create path on graph if selected isn't the targeted user */ | ||||
|     if (el === active_user) { | ||||
|       return | ||||
|     } | ||||
|     cy.elements((element) => { | ||||
|       element.addClass('not-traversed') | ||||
|     }) | ||||
| 	const on_node_tap = (el) => { | ||||
| 		reset_graph(); | ||||
| 		/* Create path on graph if selected isn't the targeted user */ | ||||
| 		if (el === active_user) { | ||||
| 			return; | ||||
| 		} | ||||
| 		cy.elements((element) => { | ||||
| 			element.addClass("not-traversed"); | ||||
| 		}); | ||||
|  | ||||
|     cy.elements() | ||||
|       .aStar({ | ||||
|         root: el, | ||||
|         goal: active_user | ||||
|       }) | ||||
|       .path.forEach((el) => { | ||||
|         el.removeClass('not-traversed') | ||||
|         el.addClass('traversed') | ||||
|       }) | ||||
|   } | ||||
| 		for (const traversed of cy.elements().aStar({ | ||||
| 			root: el, | ||||
| 			goal: active_user, | ||||
| 		}).path) { | ||||
| 			traversed.removeClass("not-traversed"); | ||||
| 			traversed.addClass("traversed"); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
|   cy.on('tap', 'node', (tapped) => { | ||||
|     on_node_tap(tapped.target) | ||||
|   }) | ||||
|   cy.zoomingEnabled(false) | ||||
| 	cy.on("tap", "node", (tapped) => { | ||||
| 		on_node_tap(tapped.target); | ||||
| 	}); | ||||
| 	cy.zoomingEnabled(false); | ||||
|  | ||||
|   /* Add context menu */ | ||||
|   if (cy.cxtmenu === undefined) { | ||||
|     console.error( | ||||
|       "ctxmenu isn't loaded, context menu won't be available on graphs" | ||||
|     ) | ||||
|     return cy | ||||
|   } | ||||
|   cy.cxtmenu({ | ||||
|     selector: 'node', | ||||
| 	/* Add context menu */ | ||||
| 	if (cy.cxtmenu === undefined) { | ||||
| 		console.error( | ||||
| 			"ctxmenu isn't loaded, context menu won't be available on graphs", | ||||
| 		); | ||||
| 		return cy; | ||||
| 	} | ||||
| 	cy.cxtmenu({ | ||||
| 		selector: "node", | ||||
|  | ||||
|     commands: [ | ||||
|       { | ||||
|         content: '<i class="fa fa-external-link fa-2x"></i>', | ||||
|         select: function (el) { | ||||
|           window.open(el.data().profile_url, '_blank').focus() | ||||
|         } | ||||
|       }, | ||||
| 		commands: [ | ||||
| 			{ | ||||
| 				content: '<i class="fa fa-external-link fa-2x"></i>', | ||||
| 				select: (el) => { | ||||
| 					window.open(el.data().profile_url, "_blank").focus(); | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
|       { | ||||
|         content: '<span class="fa fa-mouse-pointer fa-2x"></span>', | ||||
|         select: function (el) { | ||||
|           on_node_tap(el) | ||||
|         } | ||||
|       }, | ||||
| 			{ | ||||
| 				content: '<span class="fa fa-mouse-pointer fa-2x"></span>', | ||||
| 				select: (el) => { | ||||
| 					on_node_tap(el); | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
|       { | ||||
|         content: '<i class="fa fa-eraser fa-2x"></i>', | ||||
|         select: function (el) { | ||||
|           reset_graph() | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }) | ||||
| 			{ | ||||
| 				content: '<i class="fa fa-eraser fa-2x"></i>', | ||||
| 				select: (el) => { | ||||
| 					reset_graph(); | ||||
| 				}, | ||||
| 			}, | ||||
| 		], | ||||
| 	}); | ||||
|  | ||||
|   return cy | ||||
| 	return cy; | ||||
| } | ||||
|  | ||||
| /* global api_url, active_user, depth_min, depth_max */ | ||||
| document.addEventListener('alpine:init', () => { | ||||
|   /* | ||||
| document.addEventListener("alpine:init", () => { | ||||
| 	/* | ||||
|     This needs some constants to be set before the document has been loaded | ||||
|  | ||||
|     api_url:     base url for fetching the tree as a string | ||||
| @@ -174,104 +169,104 @@ document.addEventListener('alpine:init', () => { | ||||
|     depth_min:   minimum tree depth for godfathers and godchildren as an int | ||||
|     depth_max:   maximum tree depth for godfathers and godchildren as an int | ||||
|   */ | ||||
|   const default_depth = 2 | ||||
| 	const default_depth = 2; | ||||
|  | ||||
|   if ( | ||||
|     typeof api_url === 'undefined' || | ||||
|     typeof active_user === 'undefined' || | ||||
|     typeof depth_min === 'undefined' || | ||||
|     typeof depth_max === 'undefined' | ||||
|   ) { | ||||
|     console.error( | ||||
|       'Some constants are not set before using the family_graph script, please look at the documentation' | ||||
|     ) | ||||
|     return | ||||
|   } | ||||
| 	if ( | ||||
| 		typeof api_url === "undefined" || | ||||
| 		typeof active_user === "undefined" || | ||||
| 		typeof depth_min === "undefined" || | ||||
| 		typeof depth_max === "undefined" | ||||
| 	) { | ||||
| 		console.error( | ||||
| 			"Some constants are not set before using the family_graph script, please look at the documentation", | ||||
| 		); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
|   function get_initial_depth (prop) { | ||||
|     const value = parseInt(initialUrlParams.get(prop)) | ||||
|     if (isNaN(value) || value < depth_min || value > depth_max) { | ||||
|       return default_depth | ||||
|     } | ||||
|     return value | ||||
|   } | ||||
| 	function get_initial_depth(prop) { | ||||
| 		const value = Number.parseInt(initialUrlParams.get(prop)); | ||||
| 		if (Number.isNaN(value) || value < depth_min || value > depth_max) { | ||||
| 			return default_depth; | ||||
| 		} | ||||
| 		return value; | ||||
| 	} | ||||
|  | ||||
|   Alpine.data('graph', () => ({ | ||||
|     loading: false, | ||||
|     godfathers_depth: get_initial_depth('godfathers_depth'), | ||||
|     godchildren_depth: get_initial_depth('godchildren_depth'), | ||||
|     reverse: initialUrlParams.get('reverse')?.toLowerCase?.() === 'true', | ||||
|     graph: undefined, | ||||
|     graph_data: {}, | ||||
| 	Alpine.data("graph", () => ({ | ||||
| 		loading: false, | ||||
| 		godfathers_depth: get_initial_depth("godfathers_depth"), | ||||
| 		godchildren_depth: get_initial_depth("godchildren_depth"), | ||||
| 		reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true", | ||||
| 		graph: undefined, | ||||
| 		graph_data: {}, | ||||
|  | ||||
|     async init () { | ||||
|       const delayed_fetch = Alpine.debounce(async () => { | ||||
|         this.fetch_graph_data() | ||||
|       }, 100); | ||||
|       ['godfathers_depth', 'godchildren_depth'].forEach((param) => { | ||||
|         this.$watch(param, async (value) => { | ||||
|           if (value < depth_min || value > depth_max) { | ||||
|             return | ||||
|           } | ||||
|           update_query_string(param, value, History.REPLACE) | ||||
|           delayed_fetch() | ||||
|         }) | ||||
|       }) | ||||
|       this.$watch('reverse', async (value) => { | ||||
|         update_query_string('reverse', value, History.REPLACE) | ||||
|         this.reverse_graph() | ||||
|       }) | ||||
|       this.$watch('graph_data', async () => { | ||||
|         await this.generate_graph() | ||||
|         if (this.reverse) { | ||||
|           await this.reverse_graph() | ||||
|         } | ||||
|       }) | ||||
|       this.fetch_graph_data() | ||||
|     }, | ||||
| 		async init() { | ||||
| 			const delayed_fetch = Alpine.debounce(async () => { | ||||
| 				this.fetch_graph_data(); | ||||
| 			}, 100); | ||||
| 			for (const param of ["godfathers_depth", "godchildren_depth"]) { | ||||
| 				this.$watch(param, async (value) => { | ||||
| 					if (value < depth_min || value > depth_max) { | ||||
| 						return; | ||||
| 					} | ||||
| 					update_query_string(param, value, History.REPLACE); | ||||
| 					delayed_fetch(); | ||||
| 				}); | ||||
| 			} | ||||
| 			this.$watch("reverse", async (value) => { | ||||
| 				update_query_string("reverse", value, History.REPLACE); | ||||
| 				this.reverse_graph(); | ||||
| 			}); | ||||
| 			this.$watch("graph_data", async () => { | ||||
| 				await this.generate_graph(); | ||||
| 				if (this.reverse) { | ||||
| 					await this.reverse_graph(); | ||||
| 				} | ||||
| 			}); | ||||
| 			this.fetch_graph_data(); | ||||
| 		}, | ||||
|  | ||||
|     async screenshot () { | ||||
|       const link = document.createElement('a') | ||||
|       link.href = this.graph.jpg() | ||||
|       link.download = interpolate( | ||||
|         gettext('family_tree.%(extension)s'), | ||||
|         { extension: 'jpg' }, | ||||
|         true | ||||
|       ) | ||||
|       document.body.appendChild(link) | ||||
|       link.click() | ||||
|       document.body.removeChild(link) | ||||
|     }, | ||||
| 		async screenshot() { | ||||
| 			const link = document.createElement("a"); | ||||
| 			link.href = this.graph.jpg(); | ||||
| 			link.download = interpolate( | ||||
| 				gettext("family_tree.%(extension)s"), | ||||
| 				{ extension: "jpg" }, | ||||
| 				true, | ||||
| 			); | ||||
| 			document.body.appendChild(link); | ||||
| 			link.click(); | ||||
| 			document.body.removeChild(link); | ||||
| 		}, | ||||
|  | ||||
|     async reset () { | ||||
|       this.reverse = false | ||||
|       this.godfathers_depth = default_depth | ||||
|       this.godchildren_depth = default_depth | ||||
|     }, | ||||
| 		async reset() { | ||||
| 			this.reverse = false; | ||||
| 			this.godfathers_depth = default_depth; | ||||
| 			this.godchildren_depth = default_depth; | ||||
| 		}, | ||||
|  | ||||
|     async reverse_graph () { | ||||
|       this.graph.elements((el) => { | ||||
|         el.position({ x: -el.position().x, y: -el.position().y }) | ||||
|       }) | ||||
|       this.graph.center(this.graph.elements()) | ||||
|     }, | ||||
| 		async reverse_graph() { | ||||
| 			this.graph.elements((el) => { | ||||
| 				el.position({ x: -el.position().x, y: -el.position().y }); | ||||
| 			}); | ||||
| 			this.graph.center(this.graph.elements()); | ||||
| 		}, | ||||
|  | ||||
|     async fetch_graph_data () { | ||||
|       this.graph_data = await get_graph_data( | ||||
|         api_url, | ||||
|         this.godfathers_depth, | ||||
|         this.godchildren_depth | ||||
|       ) | ||||
|     }, | ||||
| 		async fetch_graph_data() { | ||||
| 			this.graph_data = await get_graph_data( | ||||
| 				api_url, | ||||
| 				this.godfathers_depth, | ||||
| 				this.godchildren_depth, | ||||
| 			); | ||||
| 		}, | ||||
|  | ||||
|     async generate_graph () { | ||||
|       this.loading = true | ||||
|       this.graph = create_graph( | ||||
|         $(this.$refs.graph), | ||||
|         this.graph_data, | ||||
|         active_user | ||||
|       ) | ||||
|       this.loading = false | ||||
|     } | ||||
|   })) | ||||
| }) | ||||
| 		async generate_graph() { | ||||
| 			this.loading = true; | ||||
| 			this.graph = create_graph( | ||||
| 				$(this.$refs.graph), | ||||
| 				this.graph_data, | ||||
| 				active_user, | ||||
| 			); | ||||
| 			this.loading = false; | ||||
| 		}, | ||||
| 	})); | ||||
| }); | ||||
|   | ||||
| @@ -1,113 +1,111 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| /* global DataTransfer */ | ||||
| export function alpine_webcam_builder ( | ||||
|   default_picture, | ||||
|   delete_url, | ||||
|   can_delete_picture | ||||
| function alpine_webcam_builder( | ||||
| 	default_picture, | ||||
| 	delete_url, | ||||
| 	can_delete_picture, | ||||
| ) { | ||||
|   return () => ({ | ||||
|     can_edit_picture: false, | ||||
| 	return () => ({ | ||||
| 		can_edit_picture: false, | ||||
|  | ||||
|     loading: false, | ||||
|     is_camera_enabled: false, | ||||
|     is_camera_error: false, | ||||
|     picture: null, | ||||
|     video: null, | ||||
|     picture_form: null, | ||||
| 		loading: false, | ||||
| 		is_camera_enabled: false, | ||||
| 		is_camera_error: false, | ||||
| 		picture: null, | ||||
| 		video: null, | ||||
| 		picture_form: null, | ||||
|  | ||||
|     init () { | ||||
|       this.video = this.$refs.video | ||||
|       this.picture_form = this.$refs.form.getElementsByTagName('input') | ||||
|       if (this.picture_form.length > 0) { | ||||
|         this.picture_form = this.picture_form[0] | ||||
|         this.can_edit_picture = true | ||||
| 		init() { | ||||
| 			this.video = this.$refs.video; | ||||
| 			this.picture_form = this.$refs.form.getElementsByTagName("input"); | ||||
| 			if (this.picture_form.length > 0) { | ||||
| 				this.picture_form = this.picture_form[0]; | ||||
| 				this.can_edit_picture = true; | ||||
|  | ||||
|         // Link the displayed element to the form input | ||||
|         this.picture_form.onchange = (event) => { | ||||
|           const files = event.srcElement.files | ||||
|           if (files.length > 0) { | ||||
|             this.picture = (window.URL || window.webkitURL).createObjectURL( | ||||
|               event.srcElement.files[0] | ||||
|             ) | ||||
|           } else { | ||||
|             this.picture = null | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| 				// Link the displayed element to the form input | ||||
| 				this.picture_form.onchange = (event) => { | ||||
| 					const files = event.srcElement.files; | ||||
| 					if (files.length > 0) { | ||||
| 						this.picture = (window.URL || window.webkitURL).createObjectURL( | ||||
| 							event.srcElement.files[0], | ||||
| 						); | ||||
| 					} else { | ||||
| 						this.picture = null; | ||||
| 					} | ||||
| 				}; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
|     get_picture () { | ||||
|       return this.picture || default_picture | ||||
|     }, | ||||
| 		get_picture() { | ||||
| 			return this.picture || default_picture; | ||||
| 		}, | ||||
|  | ||||
|     delete_picture () { | ||||
|       // Only remove currently displayed picture | ||||
|       if (this.picture) { | ||||
|         const list = new DataTransfer() | ||||
|         this.picture_form.files = list.files | ||||
|         this.picture_form.dispatchEvent(new Event('change')) | ||||
|         return | ||||
|       } | ||||
|       if (!can_delete_picture) { | ||||
|         return | ||||
|       } | ||||
|       // Remove user picture if correct rights are available | ||||
|       window.open(delete_url, '_self') | ||||
|     }, | ||||
| 		delete_picture() { | ||||
| 			// Only remove currently displayed picture | ||||
| 			if (this.picture) { | ||||
| 				const list = new DataTransfer(); | ||||
| 				this.picture_form.files = list.files; | ||||
| 				this.picture_form.dispatchEvent(new Event("change")); | ||||
| 				return; | ||||
| 			} | ||||
| 			if (!can_delete_picture) { | ||||
| 				return; | ||||
| 			} | ||||
| 			// Remove user picture if correct rights are available | ||||
| 			window.open(delete_url, "_self"); | ||||
| 		}, | ||||
|  | ||||
|     enable_camera () { | ||||
|       this.picture = null | ||||
|       this.loading = true | ||||
|       this.is_camera_error = false | ||||
|       navigator.mediaDevices | ||||
|         .getUserMedia({ video: true, audio: false }) | ||||
|         .then((stream) => { | ||||
|           this.loading = false | ||||
|           this.is_camera_enabled = true | ||||
|           this.video.srcObject = stream | ||||
|           this.video.play() | ||||
|         }) | ||||
|         .catch((err) => { | ||||
|           this.is_camera_error = true | ||||
|           this.loading = false | ||||
|           throw (err) | ||||
|         }) | ||||
|     }, | ||||
| 		enable_camera() { | ||||
| 			this.picture = null; | ||||
| 			this.loading = true; | ||||
| 			this.is_camera_error = false; | ||||
| 			navigator.mediaDevices | ||||
| 				.getUserMedia({ video: true, audio: false }) | ||||
| 				.then((stream) => { | ||||
| 					this.loading = false; | ||||
| 					this.is_camera_enabled = true; | ||||
| 					this.video.srcObject = stream; | ||||
| 					this.video.play(); | ||||
| 				}) | ||||
| 				.catch((err) => { | ||||
| 					this.is_camera_error = true; | ||||
| 					this.loading = false; | ||||
| 					throw err; | ||||
| 				}); | ||||
| 		}, | ||||
|  | ||||
|     take_picture () { | ||||
|       const canvas = document.createElement('canvas') | ||||
|       const context = canvas.getContext('2d') | ||||
| 		take_picture() { | ||||
| 			const canvas = document.createElement("canvas"); | ||||
| 			const context = canvas.getContext("2d"); | ||||
|  | ||||
|       /* Create the image */ | ||||
|       const settings = this.video.srcObject.getTracks()[0].getSettings() | ||||
|       canvas.width = settings.width | ||||
|       canvas.height = settings.height | ||||
|       context.drawImage(this.video, 0, 0, canvas.width, canvas.height) | ||||
| 			/* Create the image */ | ||||
| 			const settings = this.video.srcObject.getTracks()[0].getSettings(); | ||||
| 			canvas.width = settings.width; | ||||
| 			canvas.height = settings.height; | ||||
| 			context.drawImage(this.video, 0, 0, canvas.width, canvas.height); | ||||
|  | ||||
|       /* Stop camera */ | ||||
|       this.video.pause() | ||||
|       this.video.srcObject.getTracks().forEach((track) => { | ||||
|         if (track.readyState === 'live') { | ||||
|           track.stop() | ||||
|         } | ||||
|       }) | ||||
| 			/* Stop camera */ | ||||
| 			this.video.pause(); | ||||
| 			for (const track of this.video.srcObject.getTracks()) { | ||||
| 				if (track.readyState === "live") { | ||||
| 					track.stop(); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
|       canvas.toBlob((blob) => { | ||||
|         const filename = interpolate(gettext('captured.%s'), ['webp']) | ||||
|         const file = new File([blob], filename, { | ||||
|           type: 'image/webp' | ||||
|         }) | ||||
| 			canvas.toBlob((blob) => { | ||||
| 				const filename = interpolate(gettext("captured.%s"), ["webp"]); | ||||
| 				const file = new File([blob], filename, { | ||||
| 					type: "image/webp", | ||||
| 				}); | ||||
|  | ||||
|         const list = new DataTransfer() | ||||
|         list.items.add(file) | ||||
|         this.picture_form.files = list.files | ||||
| 				const list = new DataTransfer(); | ||||
| 				list.items.add(file); | ||||
| 				this.picture_form.files = list.files; | ||||
|  | ||||
|         // No change event is triggered, we trigger it manually #} | ||||
|         this.picture_form.dispatchEvent(new Event('change')) | ||||
|       }, 'image/webp') | ||||
| 				// No change event is triggered, we trigger it manually #} | ||||
| 				this.picture_form.dispatchEvent(new Event("change")); | ||||
| 			}, "image/webp"); | ||||
|  | ||||
|       canvas.remove() | ||||
|       this.is_camera_enabled = false | ||||
|     } | ||||
|   }) | ||||
| 			canvas.remove(); | ||||
| 			this.is_camera_enabled = false; | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import Alpine from 'alpinejs' | ||||
| import Alpine from "alpinejs"; | ||||
|  | ||||
| window.Alpine = Alpine | ||||
| window.Alpine = Alpine; | ||||
|  | ||||
| window.addEventListener('DOMContentLoaded', (event) => { | ||||
|   Alpine.start() | ||||
| }) | ||||
| window.addEventListener("DOMContentLoaded", (event) => { | ||||
| 	Alpine.start(); | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'codemirror/lib/codemirror.css' | ||||
| import 'easymde/src/css/easymde.css' | ||||
| import EasyMDE from 'easymde' | ||||
| import "codemirror/lib/codemirror.css"; | ||||
| import "easymde/src/css/easymde.css"; | ||||
| import EasyMDE from "easymde"; | ||||
|  | ||||
| // This scripts dependens on Alpine but it should be loaded on every page | ||||
|  | ||||
| @@ -9,181 +9,181 @@ import EasyMDE from 'easymde' | ||||
|  * @param {HTMLTextAreaElement} textarea to use | ||||
|  * @param {string} link to the markdown api | ||||
|  **/ | ||||
| export function easymdeFactory (textarea, markdownApiURL) { | ||||
|   const easymde = new EasyMDE({ | ||||
|     element: textarea, | ||||
|     spellChecker: false, | ||||
|     autoDownloadFontAwesome: false, | ||||
|     previewRender: Alpine.debounce(async (plainText, preview) => { | ||||
|       const res = await fetch(markdownApiURL, { | ||||
|         method: 'POST', | ||||
|         body: JSON.stringify({ text: plainText }) | ||||
|       }) | ||||
|       preview.innerHTML = await res.text() | ||||
|       return null | ||||
|     }, 300), | ||||
|     forceSync: true, // Avoid validation error on generic create view | ||||
|     toolbar: [ | ||||
|       { | ||||
|         name: 'heading-smaller', | ||||
|         action: EasyMDE.toggleHeadingSmaller, | ||||
|         className: 'fa fa-header', | ||||
|         title: gettext('Heading') | ||||
|       }, | ||||
|       { | ||||
|         name: 'italic', | ||||
|         action: EasyMDE.toggleItalic, | ||||
|         className: 'fa fa-italic', | ||||
|         title: gettext('Italic') | ||||
|       }, | ||||
|       { | ||||
|         name: 'bold', | ||||
|         action: EasyMDE.toggleBold, | ||||
|         className: 'fa fa-bold', | ||||
|         title: gettext('Bold') | ||||
|       }, | ||||
|       { | ||||
|         name: 'strikethrough', | ||||
|         action: EasyMDE.toggleStrikethrough, | ||||
|         className: 'fa fa-strikethrough', | ||||
|         title: gettext('Strikethrough') | ||||
|       }, | ||||
|       { | ||||
|         name: 'underline', | ||||
|         action: function customFunction (editor) { | ||||
|           const cm = editor.codemirror | ||||
|           cm.replaceSelection('__' + cm.getSelection() + '__') | ||||
|         }, | ||||
|         className: 'fa fa-underline', | ||||
|         title: gettext('Underline') | ||||
|       }, | ||||
|       { | ||||
|         name: 'superscript', | ||||
|         action: function customFunction (editor) { | ||||
|           const cm = editor.codemirror | ||||
|           cm.replaceSelection('^' + cm.getSelection() + '^') | ||||
|         }, | ||||
|         className: 'fa fa-superscript', | ||||
|         title: gettext('Superscript') | ||||
|       }, | ||||
|       { | ||||
|         name: 'subscript', | ||||
|         action: function customFunction (editor) { | ||||
|           const cm = editor.codemirror | ||||
|           cm.replaceSelection('~' + cm.getSelection() + '~') | ||||
|         }, | ||||
|         className: 'fa fa-subscript', | ||||
|         title: gettext('Subscript') | ||||
|       }, | ||||
|       { | ||||
|         name: 'code', | ||||
|         action: EasyMDE.toggleCodeBlock, | ||||
|         className: 'fa fa-code', | ||||
|         title: gettext('Code') | ||||
|       }, | ||||
|       '|', | ||||
|       { | ||||
|         name: 'quote', | ||||
|         action: EasyMDE.toggleBlockquote, | ||||
|         className: 'fa fa-quote-left', | ||||
|         title: gettext('Quote') | ||||
|       }, | ||||
|       { | ||||
|         name: 'unordered-list', | ||||
|         action: EasyMDE.toggleUnorderedList, | ||||
|         className: 'fa fa-list-ul', | ||||
|         title: gettext('Unordered list') | ||||
|       }, | ||||
|       { | ||||
|         name: 'ordered-list', | ||||
|         action: EasyMDE.toggleOrderedList, | ||||
|         className: 'fa fa-list-ol', | ||||
|         title: gettext('Ordered list') | ||||
|       }, | ||||
|       '|', | ||||
|       { | ||||
|         name: 'link', | ||||
|         action: EasyMDE.drawLink, | ||||
|         className: 'fa fa-link', | ||||
|         title: gettext('Insert link') | ||||
|       }, | ||||
|       { | ||||
|         name: 'image', | ||||
|         action: EasyMDE.drawImage, | ||||
|         className: 'fa-regular fa-image', | ||||
|         title: gettext('Insert image') | ||||
|       }, | ||||
|       { | ||||
|         name: 'table', | ||||
|         action: EasyMDE.drawTable, | ||||
|         className: 'fa fa-table', | ||||
|         title: gettext('Insert table') | ||||
|       }, | ||||
|       '|', | ||||
|       { | ||||
|         name: 'clean-block', | ||||
|         action: EasyMDE.cleanBlock, | ||||
|         className: 'fa fa-eraser fa-clean-block', | ||||
|         title: gettext('Clean block') | ||||
|       }, | ||||
|       '|', | ||||
|       { | ||||
|         name: 'preview', | ||||
|         action: EasyMDE.togglePreview, | ||||
|         className: 'fa fa-eye no-disable', | ||||
|         title: gettext('Toggle preview') | ||||
|       }, | ||||
|       { | ||||
|         name: 'side-by-side', | ||||
|         action: EasyMDE.toggleSideBySide, | ||||
|         className: 'fa fa-columns no-disable no-mobile', | ||||
|         title: gettext('Toggle side by side') | ||||
|       }, | ||||
|       { | ||||
|         name: 'fullscreen', | ||||
|         action: EasyMDE.toggleFullScreen, | ||||
|         className: 'fa fa-expand no-mobile', | ||||
|         title: gettext('Toggle fullscreen') | ||||
|       }, | ||||
|       '|', | ||||
|       { | ||||
|         name: 'guide', | ||||
|         action: '/page/Aide_sur_la_syntaxe', | ||||
|         className: 'fa fa-question-circle', | ||||
|         title: gettext('Markdown guide') | ||||
|       } | ||||
|     ] | ||||
|   }) | ||||
| function easymdeFactory(textarea, markdownApiURL) { | ||||
| 	const easymde = new EasyMDE({ | ||||
| 		element: textarea, | ||||
| 		spellChecker: false, | ||||
| 		autoDownloadFontAwesome: false, | ||||
| 		previewRender: Alpine.debounce(async (plainText, preview) => { | ||||
| 			const res = await fetch(markdownApiURL, { | ||||
| 				method: "POST", | ||||
| 				body: JSON.stringify({ text: plainText }), | ||||
| 			}); | ||||
| 			preview.innerHTML = await res.text(); | ||||
| 			return null; | ||||
| 		}, 300), | ||||
| 		forceSync: true, // Avoid validation error on generic create view | ||||
| 		toolbar: [ | ||||
| 			{ | ||||
| 				name: "heading-smaller", | ||||
| 				action: EasyMDE.toggleHeadingSmaller, | ||||
| 				className: "fa fa-header", | ||||
| 				title: gettext("Heading"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "italic", | ||||
| 				action: EasyMDE.toggleItalic, | ||||
| 				className: "fa fa-italic", | ||||
| 				title: gettext("Italic"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "bold", | ||||
| 				action: EasyMDE.toggleBold, | ||||
| 				className: "fa fa-bold", | ||||
| 				title: gettext("Bold"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "strikethrough", | ||||
| 				action: EasyMDE.toggleStrikethrough, | ||||
| 				className: "fa fa-strikethrough", | ||||
| 				title: gettext("Strikethrough"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "underline", | ||||
| 				action: function customFunction(editor) { | ||||
| 					const cm = editor.codemirror; | ||||
| 					cm.replaceSelection(`__${cm.getSelection()}__`); | ||||
| 				}, | ||||
| 				className: "fa fa-underline", | ||||
| 				title: gettext("Underline"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "superscript", | ||||
| 				action: function customFunction(editor) { | ||||
| 					const cm = editor.codemirror; | ||||
| 					cm.replaceSelection(`^${cm.getSelection()}^`); | ||||
| 				}, | ||||
| 				className: "fa fa-superscript", | ||||
| 				title: gettext("Superscript"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "subscript", | ||||
| 				action: function customFunction(editor) { | ||||
| 					const cm = editor.codemirror; | ||||
| 					cm.replaceSelection(`~${cm.getSelection()}~`); | ||||
| 				}, | ||||
| 				className: "fa fa-subscript", | ||||
| 				title: gettext("Subscript"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "code", | ||||
| 				action: EasyMDE.toggleCodeBlock, | ||||
| 				className: "fa fa-code", | ||||
| 				title: gettext("Code"), | ||||
| 			}, | ||||
| 			"|", | ||||
| 			{ | ||||
| 				name: "quote", | ||||
| 				action: EasyMDE.toggleBlockquote, | ||||
| 				className: "fa fa-quote-left", | ||||
| 				title: gettext("Quote"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "unordered-list", | ||||
| 				action: EasyMDE.toggleUnorderedList, | ||||
| 				className: "fa fa-list-ul", | ||||
| 				title: gettext("Unordered list"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "ordered-list", | ||||
| 				action: EasyMDE.toggleOrderedList, | ||||
| 				className: "fa fa-list-ol", | ||||
| 				title: gettext("Ordered list"), | ||||
| 			}, | ||||
| 			"|", | ||||
| 			{ | ||||
| 				name: "link", | ||||
| 				action: EasyMDE.drawLink, | ||||
| 				className: "fa fa-link", | ||||
| 				title: gettext("Insert link"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "image", | ||||
| 				action: EasyMDE.drawImage, | ||||
| 				className: "fa-regular fa-image", | ||||
| 				title: gettext("Insert image"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "table", | ||||
| 				action: EasyMDE.drawTable, | ||||
| 				className: "fa fa-table", | ||||
| 				title: gettext("Insert table"), | ||||
| 			}, | ||||
| 			"|", | ||||
| 			{ | ||||
| 				name: "clean-block", | ||||
| 				action: EasyMDE.cleanBlock, | ||||
| 				className: "fa fa-eraser fa-clean-block", | ||||
| 				title: gettext("Clean block"), | ||||
| 			}, | ||||
| 			"|", | ||||
| 			{ | ||||
| 				name: "preview", | ||||
| 				action: EasyMDE.togglePreview, | ||||
| 				className: "fa fa-eye no-disable", | ||||
| 				title: gettext("Toggle preview"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "side-by-side", | ||||
| 				action: EasyMDE.toggleSideBySide, | ||||
| 				className: "fa fa-columns no-disable no-mobile", | ||||
| 				title: gettext("Toggle side by side"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: "fullscreen", | ||||
| 				action: EasyMDE.toggleFullScreen, | ||||
| 				className: "fa fa-expand no-mobile", | ||||
| 				title: gettext("Toggle fullscreen"), | ||||
| 			}, | ||||
| 			"|", | ||||
| 			{ | ||||
| 				name: "guide", | ||||
| 				action: "/page/Aide_sur_la_syntaxe", | ||||
| 				className: "fa fa-question-circle", | ||||
| 				title: gettext("Markdown guide"), | ||||
| 			}, | ||||
| 		], | ||||
| 	}); | ||||
|  | ||||
|   const submits = textarea | ||||
|     .closest('form') | ||||
|     .querySelectorAll('input[type="submit"]') | ||||
|   const parentDiv = textarea.parentElement | ||||
|   let submitPressed = false | ||||
| 	const submits = textarea | ||||
| 		.closest("form") | ||||
| 		.querySelectorAll('input[type="submit"]'); | ||||
| 	const parentDiv = textarea.parentElement; | ||||
| 	let submitPressed = false; | ||||
|  | ||||
|   function checkMarkdownInput (e) { | ||||
|     // an attribute is null if it does not exist, else a string | ||||
|     const required = textarea.getAttribute('required') != null | ||||
|     const length = textarea.value.trim().length | ||||
| 	function checkMarkdownInput(e) { | ||||
| 		// an attribute is null if it does not exist, else a string | ||||
| 		const required = textarea.getAttribute("required") != null; | ||||
| 		const length = textarea.value.trim().length; | ||||
|  | ||||
|     if (required && length === 0) { | ||||
|       parentDiv.style.boxShadow = 'red 0px 0px 1.5px 1px' | ||||
|     } else { | ||||
|       parentDiv.style.boxShadow = '' | ||||
|     } | ||||
|   } | ||||
| 		if (required && length === 0) { | ||||
| 			parentDiv.style.boxShadow = "red 0px 0px 1.5px 1px"; | ||||
| 		} else { | ||||
| 			parentDiv.style.boxShadow = ""; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   function onSubmitClick (e) { | ||||
|     if (!submitPressed) { | ||||
|       easymde.codemirror.on('change', checkMarkdownInput) | ||||
|     } | ||||
|     submitPressed = true | ||||
|     checkMarkdownInput(e) | ||||
|   } | ||||
| 	function onSubmitClick(e) { | ||||
| 		if (!submitPressed) { | ||||
| 			easymde.codemirror.on("change", checkMarkdownInput); | ||||
| 		} | ||||
| 		submitPressed = true; | ||||
| 		checkMarkdownInput(e); | ||||
| 	} | ||||
|  | ||||
|   submits.forEach((submit) => { | ||||
|     submit.addEventListener('click', onSubmitClick) | ||||
|   }) | ||||
| }; | ||||
| 	for (const submit of submits) { | ||||
| 		submit.addEventListener("click", onSubmitClick); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| window.easymdeFactory = easymdeFactory | ||||
| window.easymdeFactory = easymdeFactory; | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| require('@fortawesome/fontawesome-free/css/all.css') | ||||
| require("@fortawesome/fontawesome-free/css/all.css"); | ||||
|   | ||||
							
								
								
									
										22
									
								
								core/static/webpack/jquery-index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								core/static/webpack/jquery-index.js
									
									
									
									
										vendored
									
									
								
							| @@ -1,25 +1,25 @@ | ||||
| import $ from 'jquery' | ||||
| import 'jquery.shorten/src/jquery.shorten.min.js' | ||||
| import $ from "jquery"; | ||||
| import "jquery.shorten/src/jquery.shorten.min.js"; | ||||
|  | ||||
| // We ship jquery-ui with jquery because when standalone with webpack | ||||
| // JQuery is also included in the jquery-ui package. We do gain space by doing this | ||||
| // We require jquery-ui components manually and not in a loop | ||||
| // Otherwise it increases the output files by a x2 factor ! | ||||
| require('jquery-ui/ui/widgets/accordion.js') | ||||
| require('jquery-ui/ui/widgets/autocomplete.js') | ||||
| require('jquery-ui/ui/widgets/button.js') | ||||
| require('jquery-ui/ui/widgets/dialog.js') | ||||
| require('jquery-ui/ui/widgets/tabs.js') | ||||
| require("jquery-ui/ui/widgets/accordion.js"); | ||||
| require("jquery-ui/ui/widgets/autocomplete.js"); | ||||
| require("jquery-ui/ui/widgets/button.js"); | ||||
| require("jquery-ui/ui/widgets/dialog.js"); | ||||
| require("jquery-ui/ui/widgets/tabs.js"); | ||||
|  | ||||
| require('jquery-ui/themes/base/all.css') | ||||
| require("jquery-ui/themes/base/all.css"); | ||||
|  | ||||
| /** | ||||
|  * Simple wrapper to solve shorten not being able on legacy pages | ||||
|  * @param {string} selector to be passed to jQuery | ||||
|  * @param {Object} options object to pass to the shorten function | ||||
|  **/ | ||||
| export function shorten (selector, options) { | ||||
|   $(selector).shorten(options) | ||||
| function shorten(selector, options) { | ||||
| 	$(selector).shorten(options); | ||||
| } | ||||
|  | ||||
| window.shorten = shorten | ||||
| window.shorten = shorten; | ||||
|   | ||||
| @@ -1,79 +1,80 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| /* global basket, click_api_url, csrf_token, products_autocomplete */ | ||||
| document.addEventListener('alpine:init', () => { | ||||
|   Alpine.data('counter', () => ({ | ||||
|     basket, | ||||
|     errors: [], | ||||
| document.addEventListener("alpine:init", () => { | ||||
| 	Alpine.data("counter", () => ({ | ||||
| 		basket, | ||||
| 		errors: [], | ||||
|  | ||||
|     sum_basket () { | ||||
|       if (!this.basket || Object.keys(this.basket).length === 0) { | ||||
|         return 0 | ||||
|       } | ||||
|       const total = Object.values(this.basket) | ||||
|         .reduce((acc, cur) => acc + cur.qty * cur.price, 0) | ||||
|       return total / 100 | ||||
|     }, | ||||
| 		sum_basket() { | ||||
| 			if (!this.basket || Object.keys(this.basket).length === 0) { | ||||
| 				return 0; | ||||
| 			} | ||||
| 			const total = Object.values(this.basket).reduce( | ||||
| 				(acc, cur) => acc + cur.qty * cur.price, | ||||
| 				0, | ||||
| 			); | ||||
| 			return total / 100; | ||||
| 		}, | ||||
|  | ||||
|     async handle_code (event) { | ||||
|       const code = $(event.target).find('#code_field').val().toUpperCase() | ||||
|       if (['FIN', 'ANN'].includes(code)) { | ||||
|         $(event.target).submit() | ||||
|       } else { | ||||
|         await this.handle_action(event) | ||||
|       } | ||||
|     }, | ||||
| 		async handle_code(event) { | ||||
| 			const code = $(event.target).find("#code_field").val().toUpperCase(); | ||||
| 			if (["FIN", "ANN"].includes(code)) { | ||||
| 				$(event.target).submit(); | ||||
| 			} else { | ||||
| 				await this.handle_action(event); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
|     async handle_action (event) { | ||||
|       const payload = $(event.target).serialize() | ||||
|       const request = new Request(click_api_url, { | ||||
|         method: 'POST', | ||||
|         body: payload, | ||||
|         headers: { | ||||
|           Accept: 'application/json', | ||||
|           'X-CSRFToken': csrf_token | ||||
|         } | ||||
|       }) | ||||
|       const response = await fetch(request) | ||||
|       const json = await response.json() | ||||
|       this.basket = json.basket | ||||
|       this.errors = json.errors | ||||
|       $('form.code_form #code_field').val('').focus() | ||||
|     } | ||||
|   })) | ||||
| }) | ||||
| 		async handle_action(event) { | ||||
| 			const payload = $(event.target).serialize(); | ||||
| 			const request = new Request(click_api_url, { | ||||
| 				method: "POST", | ||||
| 				body: payload, | ||||
| 				headers: { | ||||
| 					Accept: "application/json", | ||||
| 					"X-CSRFToken": csrf_token, | ||||
| 				}, | ||||
| 			}); | ||||
| 			const response = await fetch(request); | ||||
| 			const json = await response.json(); | ||||
| 			this.basket = json.basket; | ||||
| 			this.errors = json.errors; | ||||
| 			$("form.code_form #code_field").val("").focus(); | ||||
| 		}, | ||||
| 	})); | ||||
| }); | ||||
|  | ||||
| $(function () { | ||||
|   /* Autocompletion in the code field */ | ||||
|   const code_field = $('#code_field') | ||||
| $(() => { | ||||
| 	/* Autocompletion in the code field */ | ||||
| 	const code_field = $("#code_field"); | ||||
|  | ||||
|   let quantity = '' | ||||
|   code_field.autocomplete({ | ||||
|     select: function (event, ui) { | ||||
|       event.preventDefault() | ||||
|       code_field.val(quantity + ui.item.value) | ||||
|     }, | ||||
|     focus: function (event, ui) { | ||||
|       event.preventDefault() | ||||
|       code_field.val(quantity + ui.item.value) | ||||
|     }, | ||||
|     source: function (request, response) { | ||||
|       const res = /^(\d+x)?(.*)/i.exec(request.term) | ||||
|       quantity = res[1] || '' | ||||
|       const search = res[2] | ||||
|       const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), 'i') | ||||
|       response($.grep(products_autocomplete, function (value) { | ||||
|         value = value.tags | ||||
|         return matcher.test(value) | ||||
|       })) | ||||
|     } | ||||
|   }) | ||||
| 	let quantity = ""; | ||||
| 	code_field.autocomplete({ | ||||
| 		select: (event, ui) => { | ||||
| 			event.preventDefault(); | ||||
| 			code_field.val(quantity + ui.item.value); | ||||
| 		}, | ||||
| 		focus: (event, ui) => { | ||||
| 			event.preventDefault(); | ||||
| 			code_field.val(quantity + ui.item.value); | ||||
| 		}, | ||||
| 		source: (request, response) => { | ||||
| 			const res = /^(\d+x)?(.*)/i.exec(request.term); | ||||
| 			quantity = res[1] || ""; | ||||
| 			const search = res[2]; | ||||
| 			const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i"); | ||||
| 			response( | ||||
| 				$.grep(products_autocomplete, (value) => { | ||||
| 					return matcher.test(value.tags); | ||||
| 				}), | ||||
| 			); | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
|   /* Accordion UI between basket and refills */ | ||||
|   $('#click_form').accordion({ | ||||
|     heightStyle: 'content', | ||||
|     activate: () => $('.focus').focus() | ||||
|   }) | ||||
|   $('#products').tabs() | ||||
| 	/* Accordion UI between basket and refills */ | ||||
| 	$("#click_form").accordion({ | ||||
| 		heightStyle: "content", | ||||
| 		activate: () => $(".focus").focus(), | ||||
| 	}); | ||||
| 	$("#products").tabs(); | ||||
|  | ||||
|   code_field.focus() | ||||
| }) | ||||
| 	code_field.focus(); | ||||
| }); | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| .mermaid { | ||||
|     text-align: center; | ||||
| 	text-align: center; | ||||
| } | ||||
| @@ -1,241 +1,245 @@ | ||||
| #eboutic { | ||||
|     display: flex; | ||||
|     flex-direction: row-reverse; | ||||
|     align-items: flex-start; | ||||
|     column-gap: 20px; | ||||
|     margin: 0 20px 20px; | ||||
| 	display: flex; | ||||
| 	flex-direction: row-reverse; | ||||
| 	align-items: flex-start; | ||||
| 	column-gap: 20px; | ||||
| 	margin: 0 20px 20px; | ||||
| } | ||||
|  | ||||
| #eboutic-title { | ||||
|     margin-left: 20px; | ||||
| 	margin-left: 20px; | ||||
| } | ||||
|  | ||||
| #eboutic h3 { | ||||
|     margin-left: 0; | ||||
|     margin-right: 0; | ||||
| 	margin-left: 0; | ||||
| 	margin-right: 0; | ||||
| } | ||||
|  | ||||
| #basket { | ||||
|     min-width: 300px; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px; | ||||
|     padding: 10px; | ||||
| 	min-width: 300px; | ||||
| 	border-radius: 8px; | ||||
| 	box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px; | ||||
| 	padding: 10px; | ||||
| } | ||||
|  | ||||
| #basket h3 { | ||||
|     margin-top: 0; | ||||
| 	margin-top: 0; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 765px) { | ||||
|     #eboutic { | ||||
|         flex-direction: column-reverse; | ||||
|         align-items: center; | ||||
|         margin: 10px; | ||||
|         row-gap: 20px; | ||||
|     } | ||||
|     #eboutic-title { | ||||
|         margin-bottom: 20px; | ||||
|         margin-top: 4px; | ||||
|     } | ||||
|     #basket { | ||||
|         width: -webkit-fill-available; | ||||
|     } | ||||
| 	#eboutic { | ||||
| 		flex-direction: column-reverse; | ||||
| 		align-items: center; | ||||
| 		margin: 10px; | ||||
| 		row-gap: 20px; | ||||
| 	} | ||||
| 	#eboutic-title { | ||||
| 		margin-bottom: 20px; | ||||
| 		margin-top: 4px; | ||||
| 	} | ||||
| 	#basket { | ||||
| 		width: -webkit-fill-available; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| #eboutic .item-list { | ||||
|     margin-left: 0; | ||||
|     list-style: none; | ||||
| 	margin-left: 0; | ||||
| 	list-style: none; | ||||
| } | ||||
|  | ||||
| #eboutic .item-list li { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     margin-bottom: 10px | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| #eboutic .item-row { | ||||
|     gap: 10px; | ||||
| 	gap: 10px; | ||||
| } | ||||
|  | ||||
| #eboutic .item-name { | ||||
|     word-break: break-word; | ||||
|     width: 100%; | ||||
|     line-height: 100%; | ||||
| 	word-break: break-word; | ||||
| 	width: 100%; | ||||
| 	line-height: 100%; | ||||
| } | ||||
|  | ||||
| #eboutic .fa-plus, | ||||
| #eboutic .fa-minus { | ||||
|     cursor: pointer; | ||||
|     background-color: #354a5f; | ||||
|     color: white; | ||||
|     border-radius: 50%; | ||||
|     padding: 5px; | ||||
|     font-size: 10px; | ||||
|     line-height: 10px; | ||||
|     width: 10px; | ||||
|     text-align: center; | ||||
| 	cursor: pointer; | ||||
| 	background-color: #354a5f; | ||||
| 	color: white; | ||||
| 	border-radius: 50%; | ||||
| 	padding: 5px; | ||||
| 	font-size: 10px; | ||||
| 	line-height: 10px; | ||||
| 	width: 10px; | ||||
| 	text-align: center; | ||||
| } | ||||
|  | ||||
| #eboutic .item-quantity { | ||||
|     min-width: 65px; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     display: flex; | ||||
|     gap: 5px; | ||||
| 	min-width: 65px; | ||||
| 	justify-content: space-between; | ||||
| 	align-items: center; | ||||
| 	display: flex; | ||||
| 	gap: 5px; | ||||
| } | ||||
|  | ||||
| #eboutic .item-price { | ||||
|     min-width: 65px; | ||||
|     text-align: right; | ||||
| 	min-width: 65px; | ||||
| 	text-align: right; | ||||
| } | ||||
|  | ||||
| /*  CSS du catalogue  */ | ||||
|  | ||||
| #eboutic #catalog { | ||||
|     display: flex; | ||||
|     flex-grow: 1; | ||||
|     flex-direction: column; | ||||
|     row-gap: 30px; | ||||
| 	display: flex; | ||||
| 	flex-grow: 1; | ||||
| 	flex-direction: column; | ||||
| 	row-gap: 30px; | ||||
| } | ||||
|  | ||||
| #eboutic .category-header { | ||||
|     margin-bottom: 15px; | ||||
| 	margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| #eboutic .product-group { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     column-gap: 15px; | ||||
|     row-gap: 15px; | ||||
| 	display: flex; | ||||
| 	flex-wrap: wrap; | ||||
| 	column-gap: 15px; | ||||
| 	row-gap: 15px; | ||||
| } | ||||
| #eboutic .product-button { | ||||
|     position: relative; | ||||
|     box-sizing: border-box; | ||||
|     min-height: 180px; | ||||
|     height: fit-content; | ||||
|     width: 150px; | ||||
|     padding: 15px; | ||||
|     overflow: hidden; | ||||
|     box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     row-gap: 5px; | ||||
|     justify-content: flex-start; | ||||
| 	position: relative; | ||||
| 	box-sizing: border-box; | ||||
| 	min-height: 180px; | ||||
| 	height: fit-content; | ||||
| 	width: 150px; | ||||
| 	padding: 15px; | ||||
| 	overflow: hidden; | ||||
| 	box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px; | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	align-items: center; | ||||
| 	row-gap: 5px; | ||||
| 	justify-content: flex-start; | ||||
| } | ||||
|  | ||||
| #eboutic .product-button.selected { | ||||
|     animation: bg-in-out 1s ease; | ||||
|     background-color: rgb(216, 236, 255); | ||||
| 	animation: bg-in-out 1s ease; | ||||
| 	background-color: rgb(216, 236, 255); | ||||
| } | ||||
|  | ||||
| #eboutic .product-button.selected::after { | ||||
|     content: "🛒"; | ||||
|     position: absolute; | ||||
|     top: 5px; | ||||
|     right: 5px; | ||||
|     padding: 5px; | ||||
|     border-radius: 50%; | ||||
|     box-shadow: 0 0 12px 2px rgb(0 0 0 / 14%); | ||||
|     background-color: white; | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
|     font-size: 16px; | ||||
|     line-height: 20px; | ||||
| 	content: "🛒"; | ||||
| 	position: absolute; | ||||
| 	top: 5px; | ||||
| 	right: 5px; | ||||
| 	padding: 5px; | ||||
| 	border-radius: 50%; | ||||
| 	box-shadow: 0 0 12px 2px rgb(0 0 0 / 14%); | ||||
| 	background-color: white; | ||||
| 	width: 20px; | ||||
| 	height: 20px; | ||||
| 	font-size: 16px; | ||||
| 	line-height: 20px; | ||||
| } | ||||
|  | ||||
| #eboutic .product-button:active { | ||||
|     box-shadow: none; | ||||
| 	box-shadow: none; | ||||
| } | ||||
|  | ||||
| #eboutic .product-image { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     min-height: 70px; | ||||
|     max-height: 70px; | ||||
|     object-fit: contain; | ||||
|     border-radius: 4px; | ||||
|     line-height: 70px; | ||||
|     margin-bottom: 15px; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	min-height: 70px; | ||||
| 	max-height: 70px; | ||||
| 	object-fit: contain; | ||||
| 	border-radius: 4px; | ||||
| 	line-height: 70px; | ||||
| 	margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| #eboutic i.product-image { | ||||
|     background-color: rgba(173, 173, 173, 0.2); | ||||
| 	background-color: rgba(173, 173, 173, 0.2); | ||||
| } | ||||
|  | ||||
| #eboutic .product-description h4 { | ||||
|     font-size: .75em; | ||||
|     word-break: break-word; | ||||
|     margin: 0 0 5px 0; | ||||
| 	font-size: .75em; | ||||
| 	word-break: break-word; | ||||
| 	margin: 0 0 5px 0; | ||||
| } | ||||
|  | ||||
| #eboutic .product-button p { | ||||
|     font-size: 13px; | ||||
|     margin: 0; | ||||
| 	font-size: 13px; | ||||
| 	margin: 0; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     column-gap: 30px; | ||||
|     margin: 30px 0 0; | ||||
| 	display: flex; | ||||
| 	justify-content: center; | ||||
| 	column-gap: 30px; | ||||
| 	margin: 30px 0 0; | ||||
| } | ||||
|  | ||||
| #eboutic input { | ||||
|     all: unset; | ||||
| 	all: unset; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons button { | ||||
|     min-width: 60px; | ||||
| 	min-width: 60px; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons form { | ||||
|     margin: 0; | ||||
| 	margin: 0; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 765px) { | ||||
|     #eboutic #catalog { | ||||
|         row-gap: 15px; | ||||
|         width: 100%; | ||||
|     } | ||||
| 	#eboutic #catalog { | ||||
| 		row-gap: 15px; | ||||
| 		width: 100%; | ||||
| 	} | ||||
|  | ||||
|     #eboutic section { | ||||
|         text-align: center; | ||||
|     } | ||||
| 	#eboutic section { | ||||
| 		text-align: center; | ||||
| 	} | ||||
|  | ||||
|     #eboutic .product-group { | ||||
|         justify-content: space-around; | ||||
|         flex-direction: column; | ||||
|     } | ||||
| 	#eboutic .product-group { | ||||
| 		justify-content: space-around; | ||||
| 		flex-direction: column; | ||||
| 	} | ||||
|  | ||||
|     #eboutic .product-group .product-button { | ||||
|         min-height: 100px; | ||||
|         width: 100%; | ||||
|         max-width: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 10px; | ||||
|     } | ||||
| 	#eboutic .product-group .product-button { | ||||
| 		min-height: 100px; | ||||
| 		width: 100%; | ||||
| 		max-width: 100%; | ||||
| 		display: flex; | ||||
| 		flex-direction: row; | ||||
| 		gap: 10px; | ||||
| 	} | ||||
|  | ||||
|     #eboutic .product-group .product-description { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: flex-start; | ||||
|         width: 100%; | ||||
|     } | ||||
| 	#eboutic .product-group .product-description { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		align-items: flex-start; | ||||
| 		width: 100%; | ||||
| 	} | ||||
|  | ||||
|     #eboutic .product-description h4 { | ||||
|         text-align: left; | ||||
|         max-width: 90%; | ||||
|     } | ||||
| 	#eboutic .product-description h4 { | ||||
| 		text-align: left; | ||||
| 		max-width: 90%; | ||||
| 	} | ||||
|  | ||||
|     #eboutic .product-image { | ||||
|         margin-bottom: 0; | ||||
|         max-width: 70px; | ||||
|     } | ||||
| 	#eboutic .product-image { | ||||
| 		margin-bottom: 0; | ||||
| 		max-width: 70px; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @keyframes bg-in-out { | ||||
|     0%   { background-color: white; }  | ||||
|     100% { background-color: rgb(216, 236, 255); } | ||||
| 	0% { | ||||
| 		background-color: white; | ||||
| 	} | ||||
| 	100% { | ||||
| 		background-color: rgb(216, 236, 255); | ||||
| 	} | ||||
| } | ||||
| @@ -1,4 +1,3 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| /** | ||||
|  * @typedef {Object} BasketItem An item in the basket | ||||
|  * @property {number} id The id of the product | ||||
| @@ -7,139 +6,143 @@ | ||||
|  * @property {number} unit_price The unit price of the product | ||||
|  */ | ||||
|  | ||||
| const BASKET_ITEMS_COOKIE_NAME = 'basket_items' | ||||
| const BASKET_ITEMS_COOKIE_NAME = "basket_items"; | ||||
|  | ||||
| /** | ||||
|  * Search for a cookie by name | ||||
|  * @param {string} name Name of the cookie to get | ||||
|  * @returns {string|null|undefined} the value of the cookie or null if it does not exist, undefined if not found | ||||
|  */ | ||||
| function getCookie (name) { | ||||
|   if (!document.cookie || document.cookie.length === 0) return null | ||||
| function getCookie(name) { | ||||
| 	if (!document.cookie || document.cookie.length === 0) return null; | ||||
|  | ||||
|   const found = document.cookie | ||||
|     .split(';') | ||||
|     .map(c => c.trim()) | ||||
|     .find(c => c.startsWith(name + '=')) | ||||
| 	const found = document.cookie | ||||
| 		.split(";") | ||||
| 		.map((c) => c.trim()) | ||||
| 		.find((c) => c.startsWith(`${name}=`)); | ||||
|  | ||||
|   return found === undefined ? undefined : decodeURIComponent(found.split('=')[1]) | ||||
| 	return found === undefined | ||||
| 		? undefined | ||||
| 		: decodeURIComponent(found.split("=")[1]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Fetch the basket items from the associated cookie | ||||
|  * @returns {BasketItem[]|[]} the items in the basket | ||||
|  */ | ||||
| function get_starting_items () { | ||||
|   const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME) | ||||
|   if (!cookie) { | ||||
|     return [] | ||||
|   } | ||||
|   // Django cookie backend converts `,` to `\054` | ||||
|   let parsed = JSON.parse(cookie.replace(/\\054/g, ',')) | ||||
|   if (typeof parsed === 'string') { | ||||
|     // In some conditions, a second parsing is needed | ||||
|     parsed = JSON.parse(parsed) | ||||
|   } | ||||
|   const res = Array.isArray(parsed) ? parsed : [] | ||||
|   return res.filter((i) => !!document.getElementById(i.id)) | ||||
| function get_starting_items() { | ||||
| 	const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME); | ||||
| 	if (!cookie) { | ||||
| 		return []; | ||||
| 	} | ||||
| 	// Django cookie backend converts `,` to `\054` | ||||
| 	let parsed = JSON.parse(cookie.replace(/\\054/g, ",")); | ||||
| 	if (typeof parsed === "string") { | ||||
| 		// In some conditions, a second parsing is needed | ||||
| 		parsed = JSON.parse(parsed); | ||||
| 	} | ||||
| 	const res = Array.isArray(parsed) ? parsed : []; | ||||
| 	return res.filter((i) => !!document.getElementById(i.id)); | ||||
| } | ||||
|  | ||||
| document.addEventListener('alpine:init', () => { | ||||
|   Alpine.data('basket', () => ({ | ||||
|     items: get_starting_items(), | ||||
| document.addEventListener("alpine:init", () => { | ||||
| 	Alpine.data("basket", () => ({ | ||||
| 		items: get_starting_items(), | ||||
|  | ||||
|     /** | ||||
|          * Get the total price of the basket | ||||
|          * @returns {number} The total price of the basket | ||||
|          */ | ||||
|     get_total () { | ||||
|       return this.items | ||||
|         .reduce((acc, item) => acc + item.quantity * item.unit_price, 0) | ||||
|     }, | ||||
| 		/** | ||||
| 		 * Get the total price of the basket | ||||
| 		 * @returns {number} The total price of the basket | ||||
| 		 */ | ||||
| 		get_total() { | ||||
| 			return this.items.reduce( | ||||
| 				(acc, item) => acc + item.quantity * item.unit_price, | ||||
| 				0, | ||||
| 			); | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|          * Add 1 to the quantity of an item in the basket | ||||
|          * @param {BasketItem} item | ||||
|          */ | ||||
|     add (item) { | ||||
|       item.quantity++ | ||||
|       this.set_cookies() | ||||
|     }, | ||||
| 		/** | ||||
| 		 * Add 1 to the quantity of an item in the basket | ||||
| 		 * @param {BasketItem} item | ||||
| 		 */ | ||||
| 		add(item) { | ||||
| 			item.quantity++; | ||||
| 			this.set_cookies(); | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|          * Remove 1 to the quantity of an item in the basket | ||||
|          * @param {BasketItem} item_id | ||||
|          */ | ||||
|     remove (item_id) { | ||||
|       const index = this.items.findIndex(e => e.id === item_id) | ||||
| 		/** | ||||
| 		 * Remove 1 to the quantity of an item in the basket | ||||
| 		 * @param {BasketItem} item_id | ||||
| 		 */ | ||||
| 		remove(item_id) { | ||||
| 			const index = this.items.findIndex((e) => e.id === item_id); | ||||
|  | ||||
|       if (index < 0) return | ||||
|       this.items[index].quantity -= 1 | ||||
| 			if (index < 0) return; | ||||
| 			this.items[index].quantity -= 1; | ||||
|  | ||||
|       if (this.items[index].quantity === 0) { | ||||
|         this.items = this.items.filter((e) => e.id !== this.items[index].id) | ||||
|       } | ||||
|       this.set_cookies() | ||||
|     }, | ||||
| 			if (this.items[index].quantity === 0) { | ||||
| 				this.items = this.items.filter((e) => e.id !== this.items[index].id); | ||||
| 			} | ||||
| 			this.set_cookies(); | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|          * Remove all the items from the basket & cleans the catalog CSS classes | ||||
|          */ | ||||
|     clear_basket () { | ||||
|       this.items = [] | ||||
|       this.set_cookies() | ||||
|     }, | ||||
| 		/** | ||||
| 		 * Remove all the items from the basket & cleans the catalog CSS classes | ||||
| 		 */ | ||||
| 		clear_basket() { | ||||
| 			this.items = []; | ||||
| 			this.set_cookies(); | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|          * Set the cookie in the browser with the basket items | ||||
|          * ! the cookie survives an hour | ||||
|          */ | ||||
|     set_cookies () { | ||||
|       if (this.items.length === 0) { | ||||
|         document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0` | ||||
|       } else { | ||||
|         document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.items))};Max-Age=3600` | ||||
|       } | ||||
|     }, | ||||
| 		/** | ||||
| 		 * Set the cookie in the browser with the basket items | ||||
| 		 * ! the cookie survives an hour | ||||
| 		 */ | ||||
| 		set_cookies() { | ||||
| 			if (this.items.length === 0) { | ||||
| 				document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`; | ||||
| 			} else { | ||||
| 				document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.items))};Max-Age=3600`; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|          * Create an item in the basket if it was not already in | ||||
|          * @param {number} id The id of the product to add | ||||
|          * @param {string} name The name of the product | ||||
|          * @param {number} price The unit price of the product | ||||
|          * @returns {BasketItem} The created item | ||||
|          */ | ||||
|     create_item (id, name, price) { | ||||
|       const new_item = { | ||||
|         id, | ||||
|         name, | ||||
|         quantity: 0, | ||||
|         unit_price: price | ||||
|       } | ||||
| 		/** | ||||
| 		 * Create an item in the basket if it was not already in | ||||
| 		 * @param {number} id The id of the product to add | ||||
| 		 * @param {string} name The name of the product | ||||
| 		 * @param {number} price The unit price of the product | ||||
| 		 * @returns {BasketItem} The created item | ||||
| 		 */ | ||||
| 		create_item(id, name, price) { | ||||
| 			const new_item = { | ||||
| 				id, | ||||
| 				name, | ||||
| 				quantity: 0, | ||||
| 				unit_price: price, | ||||
| 			}; | ||||
|  | ||||
|       this.items.push(new_item) | ||||
|       this.add(new_item) | ||||
| 			this.items.push(new_item); | ||||
| 			this.add(new_item); | ||||
|  | ||||
|       return new_item | ||||
|     }, | ||||
| 			return new_item; | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|          * Add an item to the basket. | ||||
|          * This is called when the user click on a button in the catalog | ||||
|          * @param {number} id The id of the product to add | ||||
|          * @param {string} name The name of the product | ||||
|          * @param {number} price The unit price of the product | ||||
|          */ | ||||
|     add_from_catalog (id, name, price) { | ||||
|       let item = this.items.find(e => e.id === id) | ||||
| 		/** | ||||
| 		 * Add an item to the basket. | ||||
| 		 * This is called when the user click on a button in the catalog | ||||
| 		 * @param {number} id The id of the product to add | ||||
| 		 * @param {string} name The name of the product | ||||
| 		 * @param {number} price The unit price of the product | ||||
| 		 */ | ||||
| 		add_from_catalog(id, name, price) { | ||||
| 			let item = this.items.find((e) => e.id === id); | ||||
|  | ||||
|       // if the item is not in the basket, we create it | ||||
|       // else we add + 1 to it | ||||
|       if (!item) { | ||||
|         item = this.create_item(id, name, price) | ||||
|       } else { | ||||
|         this.add(item) | ||||
|       } | ||||
|     } | ||||
|   })) | ||||
| }) | ||||
| 			// if the item is not in the basket, we create it | ||||
| 			// else we add + 1 to it | ||||
| 			if (!item) { | ||||
| 				item = this.create_item(id, name, price); | ||||
| 			} else { | ||||
| 				this.add(item); | ||||
| 			} | ||||
| 		}, | ||||
| 	})); | ||||
| }); | ||||
|   | ||||
| @@ -1,82 +1,79 @@ | ||||
| /* global et_data, et_data_url, billing_info_url, | ||||
|   billing_info_success_message, billing_info_failure_message */ | ||||
|  | ||||
| /** | ||||
|  * @readonly | ||||
|  * @enum {number} | ||||
|  */ | ||||
| const BillingInfoReqState = { | ||||
|   SUCCESS: 1, | ||||
|   FAILURE: 2, | ||||
|   SENDING: 3 | ||||
| } | ||||
| 	SUCCESS: 1, | ||||
| 	FAILURE: 2, | ||||
| 	SENDING: 3, | ||||
| }; | ||||
|  | ||||
| document.addEventListener('alpine:init', () => { | ||||
|   Alpine.store('billing_inputs', { | ||||
|     data: et_data, | ||||
| document.addEventListener("alpine:init", () => { | ||||
| 	Alpine.store("billing_inputs", { | ||||
| 		data: et_data, | ||||
|  | ||||
|     async fill () { | ||||
|       document.getElementById('bank-submit-button').disabled = true | ||||
|       const res = await fetch(et_data_url) | ||||
|       if (res.ok) { | ||||
|         this.data = await res.json() | ||||
|         document.getElementById('bank-submit-button').disabled = false | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| 		async fill() { | ||||
| 			document.getElementById("bank-submit-button").disabled = true; | ||||
| 			const res = await fetch(et_data_url); | ||||
| 			if (res.ok) { | ||||
| 				this.data = await res.json(); | ||||
| 				document.getElementById("bank-submit-button").disabled = false; | ||||
| 			} | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
|   Alpine.data('billing_infos', () => ({ | ||||
|     /** @type {BillingInfoReqState | null} */ | ||||
|     req_state: null, | ||||
| 	Alpine.data("billing_infos", () => ({ | ||||
| 		/** @type {BillingInfoReqState | null} */ | ||||
| 		req_state: null, | ||||
|  | ||||
|     async send_form () { | ||||
|       this.req_state = BillingInfoReqState.SENDING | ||||
|       const form = document.getElementById('billing_info_form') | ||||
|       document.getElementById('bank-submit-button').disabled = true | ||||
|       const payload = Object.fromEntries( | ||||
|         Array.from(form.querySelectorAll('input, select')) | ||||
|           .filter((elem) => elem.type !== 'submit' && elem.value) | ||||
|           .map((elem) => [elem.name, elem.value]) | ||||
|       ) | ||||
|       const res = await fetch(billing_info_url, { | ||||
|         method: 'PUT', | ||||
|         body: JSON.stringify(payload) | ||||
|       }) | ||||
|       this.req_state = res.ok | ||||
|         ? BillingInfoReqState.SUCCESS | ||||
|         : BillingInfoReqState.FAILURE | ||||
|       if (res.status === 422) { | ||||
|         const errors = (await res.json()).detail.map((err) => err.loc).flat() | ||||
|         Array.from(form.querySelectorAll('input')) | ||||
|           .filter((elem) => errors.includes(elem.name)) | ||||
|           .forEach((elem) => { | ||||
|             elem.setCustomValidity(gettext('Incorrect value')) | ||||
|             elem.reportValidity() | ||||
|             elem.oninput = () => elem.setCustomValidity('') | ||||
|           }) | ||||
|       } else if (res.ok) { | ||||
|         Alpine.store('billing_inputs').fill() | ||||
|       } | ||||
|     }, | ||||
| 		async send_form() { | ||||
| 			this.req_state = BillingInfoReqState.SENDING; | ||||
| 			const form = document.getElementById("billing_info_form"); | ||||
| 			document.getElementById("bank-submit-button").disabled = true; | ||||
| 			const payload = Object.fromEntries( | ||||
| 				Array.from(form.querySelectorAll("input, select")) | ||||
| 					.filter((elem) => elem.type !== "submit" && elem.value) | ||||
| 					.map((elem) => [elem.name, elem.value]), | ||||
| 			); | ||||
| 			const res = await fetch(billing_info_url, { | ||||
| 				method: "PUT", | ||||
| 				body: JSON.stringify(payload), | ||||
| 			}); | ||||
| 			this.req_state = res.ok | ||||
| 				? BillingInfoReqState.SUCCESS | ||||
| 				: BillingInfoReqState.FAILURE; | ||||
| 			if (res.status === 422) { | ||||
| 				const errors = (await res.json()).detail.flatMap((err) => err.loc); | ||||
| 				for (const elem of Array.from(form.querySelectorAll("input")).filter( | ||||
| 					(elem) => errors.includes(elem.name), | ||||
| 				)) { | ||||
| 					elem.setCustomValidity(gettext("Incorrect value")); | ||||
| 					elem.reportValidity(); | ||||
| 					elem.oninput = () => elem.setCustomValidity(""); | ||||
| 				} | ||||
| 			} else if (res.ok) { | ||||
| 				Alpine.store("billing_inputs").fill(); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
|     get_alert_color () { | ||||
|       if (this.req_state === BillingInfoReqState.SUCCESS) { | ||||
|         return 'green' | ||||
|       } | ||||
|       if (this.req_state === BillingInfoReqState.FAILURE) { | ||||
|         return 'red' | ||||
|       } | ||||
|       return '' | ||||
|     }, | ||||
| 		get_alert_color() { | ||||
| 			if (this.req_state === BillingInfoReqState.SUCCESS) { | ||||
| 				return "green"; | ||||
| 			} | ||||
| 			if (this.req_state === BillingInfoReqState.FAILURE) { | ||||
| 				return "red"; | ||||
| 			} | ||||
| 			return ""; | ||||
| 		}, | ||||
|  | ||||
|     get_alert_message () { | ||||
|       if (this.req_state === BillingInfoReqState.SUCCESS) { | ||||
|         return billing_info_success_message | ||||
|       } | ||||
|       if (this.req_state === BillingInfoReqState.FAILURE) { | ||||
|         return billing_info_failure_message | ||||
|       } | ||||
|       return '' | ||||
|     } | ||||
|   })) | ||||
| }) | ||||
| 		get_alert_message() { | ||||
| 			if (this.req_state === BillingInfoReqState.SUCCESS) { | ||||
| 				return billing_info_success_message; | ||||
| 			} | ||||
| 			if (this.req_state === BillingInfoReqState.FAILURE) { | ||||
| 				return billing_info_failure_message; | ||||
| 			} | ||||
| 			return ""; | ||||
| 		}, | ||||
| 	})); | ||||
| }); | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										14362
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14362
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										81
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,47 +1,38 @@ | ||||
| { | ||||
|     "name": "sith", | ||||
|     "version": "3", | ||||
|     "description": "Le web Sith de l'AE", | ||||
|     "main": "index.js", | ||||
|     "scripts": { | ||||
|         "compile": "webpack --mode production", | ||||
|         "compile-dev": "webpack --mode development", | ||||
|         "serve": "webpack --mode development --watch" | ||||
|     }, | ||||
|     "keywords": [], | ||||
|     "author": "", | ||||
|     "license": "GPL-3.0-only", | ||||
|     "sideEffects": [ | ||||
|         ".css" | ||||
|     ], | ||||
|     "standard": { | ||||
|       "ignore": [ | ||||
|         "core/static/vendored", | ||||
|         "staticfiles/generated" | ||||
|       ], | ||||
|       "globals": [ "Alpine", "$", "jQuery", "gettext", "interpolate" ] | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@babel/core": "^7.25.2", | ||||
|         "@babel/preset-env": "^7.25.4", | ||||
|         "babel-loader": "^9.2.1", | ||||
|         "css-loader": "^7.1.2", | ||||
|         "css-minimizer-webpack-plugin": "^7.0.0", | ||||
|         "expose-loader": "^5.0.0", | ||||
|         "mini-css-extract-plugin": "^2.9.1", | ||||
|         "source-map-loader": "^5.0.0", | ||||
|         "standard": "^17.1.2", | ||||
|         "terser-webpack-plugin": "^5.3.10", | ||||
|         "webpack": "^5.94.0", | ||||
|         "webpack-cli": "^5.1.4" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@fortawesome/fontawesome-free": "^6.6.0", | ||||
|         "alpinejs": "^3.14.1", | ||||
|         "easymde": "^2.18.0", | ||||
|         "glob": "^11.0.0", | ||||
|         "jquery": "^3.7.1", | ||||
|         "jquery-ui": "^1.14.0", | ||||
|         "jquery.shorten": "^1.0.0" | ||||
|     } | ||||
| 	"name": "sith", | ||||
| 	"version": "3", | ||||
| 	"description": "Le web Sith de l'AE", | ||||
| 	"main": "index.js", | ||||
| 	"scripts": { | ||||
| 		"compile": "webpack --mode production", | ||||
| 		"compile-dev": "webpack --mode development", | ||||
| 		"serve": "webpack --mode development --watch" | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": "", | ||||
| 	"license": "GPL-3.0-only", | ||||
| 	"sideEffects": [".css"], | ||||
| 	"devDependencies": { | ||||
| 		"@babel/core": "^7.25.2", | ||||
| 		"@babel/preset-env": "^7.25.4", | ||||
| 		"@biomejs/biome": "1.9.3", | ||||
| 		"babel-loader": "^9.2.1", | ||||
| 		"css-loader": "^7.1.2", | ||||
| 		"css-minimizer-webpack-plugin": "^7.0.0", | ||||
| 		"expose-loader": "^5.0.0", | ||||
| 		"mini-css-extract-plugin": "^2.9.1", | ||||
| 		"source-map-loader": "^5.0.0", | ||||
| 		"terser-webpack-plugin": "^5.3.10", | ||||
| 		"webpack": "^5.94.0", | ||||
| 		"webpack-cli": "^5.1.4" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@fortawesome/fontawesome-free": "^6.6.0", | ||||
| 		"alpinejs": "^3.14.1", | ||||
| 		"easymde": "^2.18.0", | ||||
| 		"glob": "^11.0.0", | ||||
| 		"jquery": "^3.7.1", | ||||
| 		"jquery-ui": "^1.14.0", | ||||
| 		"jquery.shorten": "^1.0.0" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,3 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| /* global Image, History, fetch_paginated, picture_endpoint, | ||||
|   sithSelect2, remote_data_source, first_picture_id, | ||||
|   album_url, user_is_sas_admin, user_id */ | ||||
| /** | ||||
|  * @typedef PictureIdentification | ||||
|  * @property {number} id The actual id of the identification | ||||
| @@ -13,258 +9,260 @@ | ||||
|  * able to prefetch its data. | ||||
|  */ | ||||
| class PictureWithIdentifications { | ||||
|   identifications = null | ||||
|   image_loading = false | ||||
|   identifications_loading = false | ||||
| 	identifications = null; | ||||
| 	image_loading = false; | ||||
| 	identifications_loading = false; | ||||
|  | ||||
|   /** | ||||
|    * @param {Picture} picture | ||||
|    */ | ||||
|   constructor (picture) { | ||||
|     Object.assign(this, picture) | ||||
|   } | ||||
| 	/** | ||||
| 	 * @param {Picture} picture | ||||
| 	 */ | ||||
| 	constructor(picture) { | ||||
| 		Object.assign(this, picture); | ||||
| 	} | ||||
|  | ||||
|   /** | ||||
|    * @param {Picture} picture | ||||
|    */ | ||||
|   static from_picture (picture) { | ||||
|     return new PictureWithIdentifications(picture) | ||||
|   } | ||||
| 	/** | ||||
| 	 * @param {Picture} picture | ||||
| 	 */ | ||||
| 	static from_picture(picture) { | ||||
| 		return new PictureWithIdentifications(picture); | ||||
| 	} | ||||
|  | ||||
|   /** | ||||
|    * If not already done, fetch the users identified on this picture and | ||||
|    * populate the identifications field | ||||
|    * @param {?Object=} options | ||||
|    * @return {Promise<void>} | ||||
|    */ | ||||
|   async load_identifications (options) { | ||||
|     if (this.identifications_loading) { | ||||
|       return // The users are already being fetched. | ||||
|     } | ||||
|     if (!!this.identifications && !options?.force_reload) { | ||||
|       // The users are already fetched | ||||
|       // and the user does not want to force the reload | ||||
|       return | ||||
|     } | ||||
|     this.identifications_loading = true | ||||
|     const url = `/api/sas/picture/${this.id}/identified` | ||||
|     this.identifications = await (await fetch(url)).json() | ||||
|     this.identifications_loading = false | ||||
|   } | ||||
| 	/** | ||||
| 	 * If not already done, fetch the users identified on this picture and | ||||
| 	 * populate the identifications field | ||||
| 	 * @param {?Object=} options | ||||
| 	 * @return {Promise<void>} | ||||
| 	 */ | ||||
| 	async load_identifications(options) { | ||||
| 		if (this.identifications_loading) { | ||||
| 			return; // The users are already being fetched. | ||||
| 		} | ||||
| 		if (!!this.identifications && !options?.force_reload) { | ||||
| 			// The users are already fetched | ||||
| 			// and the user does not want to force the reload | ||||
| 			return; | ||||
| 		} | ||||
| 		this.identifications_loading = true; | ||||
| 		const url = `/api/sas/picture/${this.id}/identified`; | ||||
| 		this.identifications = await (await fetch(url)).json(); | ||||
| 		this.identifications_loading = false; | ||||
| 	} | ||||
|  | ||||
|   /** | ||||
|    * Preload the photo and the identifications | ||||
|    * @return {Promise<void>} | ||||
|    */ | ||||
|   async preload () { | ||||
|     const img = new Image() | ||||
|     img.src = this.compressed_url | ||||
|     if (!img.complete) { | ||||
|       this.image_loading = true | ||||
|       img.addEventListener('load', () => { | ||||
|         this.image_loading = false | ||||
|       }) | ||||
|     } | ||||
|     await this.load_identifications() | ||||
|   } | ||||
| 	/** | ||||
| 	 * Preload the photo and the identifications | ||||
| 	 * @return {Promise<void>} | ||||
| 	 */ | ||||
| 	async preload() { | ||||
| 		const img = new Image(); | ||||
| 		img.src = this.compressed_url; | ||||
| 		if (!img.complete) { | ||||
| 			this.image_loading = true; | ||||
| 			img.addEventListener("load", () => { | ||||
| 				this.image_loading = false; | ||||
| 			}); | ||||
| 		} | ||||
| 		await this.load_identifications(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| document.addEventListener('alpine:init', () => { | ||||
|   Alpine.data('picture_viewer', () => ({ | ||||
|     /** | ||||
|      * All the pictures that can be displayed on this picture viewer | ||||
|      * @type PictureWithIdentifications[] | ||||
|      **/ | ||||
|     pictures: [], | ||||
|     /** | ||||
|      * The currently displayed picture | ||||
|      * Default dummy data are pre-loaded to avoid javascript error | ||||
|      * when loading the page at the beginning | ||||
|      * @type PictureWithIdentifications | ||||
|      **/ | ||||
|     current_picture: { | ||||
|       is_moderated: true, | ||||
|       id: null, | ||||
|       name: '', | ||||
|       display_name: '', | ||||
|       compressed_url: '', | ||||
|       profile_url: '', | ||||
|       full_size_url: '', | ||||
|       owner: '', | ||||
|       date: new Date(), | ||||
|       identifications: [] | ||||
|     }, | ||||
|     /** | ||||
|      * The picture which will be displayed next if the user press the "next" button | ||||
|      * @type ?PictureWithIdentifications | ||||
|      **/ | ||||
|     next_picture: null, | ||||
|     /** | ||||
|      * The picture which will be displayed next if the user press the "previous" button | ||||
|      * @type ?PictureWithIdentifications | ||||
|      **/ | ||||
|     previous_picture: null, | ||||
|     /** | ||||
|      * The select2 component used to identify users | ||||
|      **/ | ||||
|     selector: undefined, | ||||
|     /** | ||||
|      * true if the page is in a loading state, else false | ||||
|      **/ | ||||
|     /** | ||||
|      * Error message when a moderation operation fails | ||||
|      * @type string | ||||
|      **/ | ||||
|     moderation_error: '', | ||||
|     /** | ||||
|      * Method of pushing new url to the browser history | ||||
|      * Used by popstate event and always reset to it's default value when used | ||||
|      * @type History | ||||
|      **/ | ||||
|     pushstate: History.PUSH, | ||||
| document.addEventListener("alpine:init", () => { | ||||
| 	Alpine.data("picture_viewer", () => ({ | ||||
| 		/** | ||||
| 		 * All the pictures that can be displayed on this picture viewer | ||||
| 		 * @type PictureWithIdentifications[] | ||||
| 		 **/ | ||||
| 		pictures: [], | ||||
| 		/** | ||||
| 		 * The currently displayed picture | ||||
| 		 * Default dummy data are pre-loaded to avoid javascript error | ||||
| 		 * when loading the page at the beginning | ||||
| 		 * @type PictureWithIdentifications | ||||
| 		 **/ | ||||
| 		current_picture: { | ||||
| 			is_moderated: true, | ||||
| 			id: null, | ||||
| 			name: "", | ||||
| 			display_name: "", | ||||
| 			compressed_url: "", | ||||
| 			profile_url: "", | ||||
| 			full_size_url: "", | ||||
| 			owner: "", | ||||
| 			date: new Date(), | ||||
| 			identifications: [], | ||||
| 		}, | ||||
| 		/** | ||||
| 		 * The picture which will be displayed next if the user press the "next" button | ||||
| 		 * @type ?PictureWithIdentifications | ||||
| 		 **/ | ||||
| 		next_picture: null, | ||||
| 		/** | ||||
| 		 * The picture which will be displayed next if the user press the "previous" button | ||||
| 		 * @type ?PictureWithIdentifications | ||||
| 		 **/ | ||||
| 		previous_picture: null, | ||||
| 		/** | ||||
| 		 * The select2 component used to identify users | ||||
| 		 **/ | ||||
| 		selector: undefined, | ||||
| 		/** | ||||
| 		 * true if the page is in a loading state, else false | ||||
| 		 **/ | ||||
| 		/** | ||||
| 		 * Error message when a moderation operation fails | ||||
| 		 * @type string | ||||
| 		 **/ | ||||
| 		moderation_error: "", | ||||
| 		/** | ||||
| 		 * Method of pushing new url to the browser history | ||||
| 		 * Used by popstate event and always reset to it's default value when used | ||||
| 		 * @type History | ||||
| 		 **/ | ||||
| 		pushstate: History.PUSH, | ||||
|  | ||||
|     async init () { | ||||
|       this.pictures = (await fetch_paginated(picture_endpoint)).map( | ||||
|         PictureWithIdentifications.from_picture | ||||
|       ) | ||||
|       this.selector = sithSelect2({ | ||||
|         element: $(this.$refs.search), | ||||
|         data_source: remote_data_source('/api/user/search', { | ||||
|           excluded: () => [ | ||||
|             ...(this.current_picture.identifications || []).map( | ||||
|               (i) => i.user.id | ||||
|             ) | ||||
|           ], | ||||
|           result_converter: (obj) => Object({ ...obj, text: obj.display_name }) | ||||
|         }), | ||||
|         picture_getter: (user) => user.profile_pict | ||||
|       }) | ||||
|       this.current_picture = this.pictures.find( | ||||
|         (i) => i.id === first_picture_id | ||||
|       ) | ||||
|       this.$watch('current_picture', (current, previous) => { | ||||
|         if (current === previous) { /* Avoid recursive updates */ | ||||
|           return | ||||
|         } | ||||
|         this.update_picture() | ||||
|       }) | ||||
|       window.addEventListener('popstate', async (event) => { | ||||
|         if (!event.state || event.state.sas_picture_id === undefined) { | ||||
|           return | ||||
|         } | ||||
|         this.pushstate = History.REPLACE | ||||
|         this.current_picture = this.pictures.find( | ||||
|           (i) => i.id === parseInt(event.state.sas_picture_id) | ||||
|         ) | ||||
|       }) | ||||
|       this.pushstate = History.REPLACE /* Avoid first url push */ | ||||
|       await this.update_picture() | ||||
|     }, | ||||
| 		async init() { | ||||
| 			this.pictures = (await fetch_paginated(picture_endpoint)).map( | ||||
| 				PictureWithIdentifications.from_picture, | ||||
| 			); | ||||
| 			this.selector = sithSelect2({ | ||||
| 				element: $(this.$refs.search), | ||||
| 				data_source: remote_data_source("/api/user/search", { | ||||
| 					excluded: () => [ | ||||
| 						...(this.current_picture.identifications || []).map( | ||||
| 							(i) => i.user.id, | ||||
| 						), | ||||
| 					], | ||||
| 					result_converter: (obj) => Object({ ...obj, text: obj.display_name }), | ||||
| 				}), | ||||
| 				picture_getter: (user) => user.profile_pict, | ||||
| 			}); | ||||
| 			this.current_picture = this.pictures.find( | ||||
| 				(i) => i.id === first_picture_id, | ||||
| 			); | ||||
| 			this.$watch("current_picture", (current, previous) => { | ||||
| 				if (current === previous) { | ||||
| 					/* Avoid recursive updates */ | ||||
| 					return; | ||||
| 				} | ||||
| 				this.update_picture(); | ||||
| 			}); | ||||
| 			window.addEventListener("popstate", async (event) => { | ||||
| 				if (!event.state || event.state.sas_picture_id === undefined) { | ||||
| 					return; | ||||
| 				} | ||||
| 				this.pushstate = History.REPLACE; | ||||
| 				this.current_picture = this.pictures.find( | ||||
| 					(i) => i.id === Number.parseInt(event.state.sas_picture_id), | ||||
| 				); | ||||
| 			}); | ||||
| 			this.pushstate = History.REPLACE; /* Avoid first url push */ | ||||
| 			await this.update_picture(); | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|      * Update the page. | ||||
|      * Called when the `current_picture` property changes. | ||||
|      * | ||||
|      * The url is modified without reloading the page, | ||||
|      * and the previous picture, the next picture and | ||||
|      * the list of identified users are updated. | ||||
|      */ | ||||
|     async update_picture () { | ||||
|       const update_args = [ | ||||
|         { sas_picture_id: this.current_picture.id }, | ||||
|         '', | ||||
|         `/sas/picture/${this.current_picture.id}/` | ||||
|       ] | ||||
|       if (this.pushstate === History.REPLACE) { | ||||
|         window.history.replaceState(...update_args) | ||||
|         this.pushstate = History.PUSH | ||||
|       } else { | ||||
|         window.history.pushState(...update_args) | ||||
|       } | ||||
| 		/** | ||||
| 		 * Update the page. | ||||
| 		 * Called when the `current_picture` property changes. | ||||
| 		 * | ||||
| 		 * The url is modified without reloading the page, | ||||
| 		 * and the previous picture, the next picture and | ||||
| 		 * the list of identified users are updated. | ||||
| 		 */ | ||||
| 		async update_picture() { | ||||
| 			const update_args = [ | ||||
| 				{ sas_picture_id: this.current_picture.id }, | ||||
| 				"", | ||||
| 				`/sas/picture/${this.current_picture.id}/`, | ||||
| 			]; | ||||
| 			if (this.pushstate === History.REPLACE) { | ||||
| 				window.history.replaceState(...update_args); | ||||
| 				this.pushstate = History.PUSH; | ||||
| 			} else { | ||||
| 				window.history.pushState(...update_args); | ||||
| 			} | ||||
|  | ||||
|       this.moderation_error = '' | ||||
|       const index = this.pictures.indexOf(this.current_picture) | ||||
|       this.previous_picture = this.pictures[index - 1] || null | ||||
|       this.next_picture = this.pictures[index + 1] || null | ||||
|       await this.current_picture.load_identifications() | ||||
|       this.$refs.main_picture?.addEventListener('load', () => { | ||||
|         // once the current picture is loaded, | ||||
|         // start preloading the next and previous pictures | ||||
|         this.next_picture?.preload() | ||||
|         this.previous_picture?.preload() | ||||
|       }) | ||||
|     }, | ||||
| 			this.moderation_error = ""; | ||||
| 			const index = this.pictures.indexOf(this.current_picture); | ||||
| 			this.previous_picture = this.pictures[index - 1] || null; | ||||
| 			this.next_picture = this.pictures[index + 1] || null; | ||||
| 			await this.current_picture.load_identifications(); | ||||
| 			this.$refs.main_picture?.addEventListener("load", () => { | ||||
| 				// once the current picture is loaded, | ||||
| 				// start preloading the next and previous pictures | ||||
| 				this.next_picture?.preload(); | ||||
| 				this.previous_picture?.preload(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
|     async moderate_picture () { | ||||
|       const res = await fetch( | ||||
|         `/api/sas/picture/${this.current_picture.id}/moderate`, | ||||
|         { | ||||
|           method: 'PATCH' | ||||
|         } | ||||
|       ) | ||||
|       if (!res.ok) { | ||||
|         this.moderation_error = `${gettext("Couldn't moderate picture")} : ${res.statusText}` | ||||
|         return | ||||
|       } | ||||
|       this.current_picture.is_moderated = true | ||||
|       this.current_picture.asked_for_removal = false | ||||
|     }, | ||||
| 		async moderate_picture() { | ||||
| 			const res = await fetch( | ||||
| 				`/api/sas/picture/${this.current_picture.id}/moderate`, | ||||
| 				{ | ||||
| 					method: "PATCH", | ||||
| 				}, | ||||
| 			); | ||||
| 			if (!res.ok) { | ||||
| 				this.moderation_error = `${gettext("Couldn't moderate picture")} : ${res.statusText}`; | ||||
| 				return; | ||||
| 			} | ||||
| 			this.current_picture.is_moderated = true; | ||||
| 			this.current_picture.asked_for_removal = false; | ||||
| 		}, | ||||
|  | ||||
|     async delete_picture () { | ||||
|       const res = await fetch(`/api/sas/picture/${this.current_picture.id}`, { | ||||
|         method: 'DELETE' | ||||
|       }) | ||||
|       if (!res.ok) { | ||||
|         this.moderation_error = | ||||
|           gettext("Couldn't delete picture") + ' : ' + res.statusText | ||||
|         return | ||||
|       } | ||||
|       this.pictures.splice(this.pictures.indexOf(this.current_picture), 1) | ||||
|       if (this.pictures.length === 0) { | ||||
|         // The deleted picture was the only one in the list. | ||||
|         // As the album is now empty, go back to the parent page | ||||
|         document.location.href = album_url | ||||
|       } | ||||
|       this.current_picture = this.next_picture || this.previous_picture | ||||
|     }, | ||||
| 		async delete_picture() { | ||||
| 			const res = await fetch(`/api/sas/picture/${this.current_picture.id}`, { | ||||
| 				method: "DELETE", | ||||
| 			}); | ||||
| 			if (!res.ok) { | ||||
| 				this.moderation_error = `${gettext("Couldn't delete picture")} : ${res.statusText}`; | ||||
| 				return; | ||||
| 			} | ||||
| 			this.pictures.splice(this.pictures.indexOf(this.current_picture), 1); | ||||
| 			if (this.pictures.length === 0) { | ||||
| 				// The deleted picture was the only one in the list. | ||||
| 				// As the album is now empty, go back to the parent page | ||||
| 				document.location.href = album_url; | ||||
| 			} | ||||
| 			this.current_picture = this.next_picture || this.previous_picture; | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|      * Send the identification request and update the list of identified users. | ||||
|      */ | ||||
|     async submit_identification () { | ||||
|       const url = `/api/sas/picture/${this.current_picture.id}/identified` | ||||
|       await fetch(url, { | ||||
|         method: 'PUT', | ||||
|         body: JSON.stringify(this.selector.val().map((i) => parseInt(i))) | ||||
|       }) | ||||
|       // refresh the identified users list | ||||
|       await this.current_picture.load_identifications({ force_reload: true }) | ||||
|       this.selector.empty().trigger('change') | ||||
|     }, | ||||
| 		/** | ||||
| 		 * Send the identification request and update the list of identified users. | ||||
| 		 */ | ||||
| 		async submit_identification() { | ||||
| 			const url = `/api/sas/picture/${this.current_picture.id}/identified`; | ||||
| 			await fetch(url, { | ||||
| 				method: "PUT", | ||||
| 				body: JSON.stringify( | ||||
| 					this.selector.val().map((i) => Number.parseInt(i)), | ||||
| 				), | ||||
| 			}); | ||||
| 			// refresh the identified users list | ||||
| 			await this.current_picture.load_identifications({ force_reload: true }); | ||||
| 			this.selector.empty().trigger("change"); | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|      * Check if an identification can be removed by the currently logged user | ||||
|      * @param {PictureIdentification} identification | ||||
|      * @return {boolean} | ||||
|      */ | ||||
|     can_be_removed (identification) { | ||||
|       return user_is_sas_admin || identification.user.id === user_id | ||||
|     }, | ||||
| 		/** | ||||
| 		 * Check if an identification can be removed by the currently logged user | ||||
| 		 * @param {PictureIdentification} identification | ||||
| 		 * @return {boolean} | ||||
| 		 */ | ||||
| 		can_be_removed(identification) { | ||||
| 			return user_is_sas_admin || identification.user.id === user_id; | ||||
| 		}, | ||||
|  | ||||
|     /** | ||||
|      * Untag a user from the current picture | ||||
|      * @param {PictureIdentification} identification | ||||
|      */ | ||||
|     async remove_identification (identification) { | ||||
|       const res = await fetch(`/api/sas/relation/${identification.id}`, { | ||||
|         method: 'DELETE' | ||||
|       }) | ||||
|       if (res.ok && Array.isArray(this.current_picture.identifications)) { | ||||
|         this.current_picture.identifications = | ||||
|           this.current_picture.identifications.filter( | ||||
|             (i) => i.id !== identification.id | ||||
|           ) | ||||
|       } | ||||
|     } | ||||
|   })) | ||||
| }) | ||||
| 		/** | ||||
| 		 * Untag a user from the current picture | ||||
| 		 * @param {PictureIdentification} identification | ||||
| 		 */ | ||||
| 		async remove_identification(identification) { | ||||
| 			const res = await fetch(`/api/sas/relation/${identification.id}`, { | ||||
| 				method: "DELETE", | ||||
| 			}); | ||||
| 			if (res.ok && Array.isArray(this.current_picture.identifications)) { | ||||
| 				this.current_picture.identifications = | ||||
| 					this.current_picture.identifications.filter( | ||||
| 						(i) => i.id !== identification.id, | ||||
| 					); | ||||
| 			} | ||||
| 		}, | ||||
| 	})); | ||||
| }); | ||||
|   | ||||
| @@ -1,88 +1,85 @@ | ||||
| const glob = require('glob') | ||||
| const path = require('path') | ||||
| const MiniCssExtractPlugin = require('mini-css-extract-plugin') | ||||
| const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') | ||||
| const TerserPlugin = require('terser-webpack-plugin') | ||||
| const glob = require("glob"); | ||||
| const path = require("node:path"); | ||||
| const MiniCssExtractPlugin = require("mini-css-extract-plugin"); | ||||
| const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); | ||||
| const TerserPlugin = require("terser-webpack-plugin"); | ||||
|  | ||||
| module.exports = { | ||||
|   entry: glob.sync('./!(static)/static/webpack/**?(-)index.js').reduce((obj, el) => { | ||||
|     obj[path.parse(el).name] = './' + el | ||||
|     return obj | ||||
|   }, {}), | ||||
|   output: { | ||||
|     filename: '[name].js', | ||||
|     path: path.resolve(__dirname, './staticfiles/generated/webpack'), | ||||
|     clean: true | ||||
|   }, | ||||
|   plugins: [ | ||||
|     new MiniCssExtractPlugin() | ||||
|   ], | ||||
|   optimization: { | ||||
|     minimizer: [ | ||||
|       '...', | ||||
|       new CssMinimizerPlugin({ | ||||
|         parallel: true | ||||
|       }), | ||||
|       new TerserPlugin({ | ||||
|         parallel: true, | ||||
|         terserOptions: { | ||||
|           mangle: true, | ||||
|           compress: { | ||||
|             drop_console: true | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     ] | ||||
|   }, | ||||
|   module: { | ||||
|     rules: [ | ||||
|       { | ||||
|         test: /\.css$/, | ||||
|         sideEffects: true, | ||||
|         use: [ | ||||
|           MiniCssExtractPlugin.loader, | ||||
|           'css-loader' | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         test: /\.(jpe?g|png|gif)$/i, | ||||
|         type: 'asset/resource' | ||||
|       }, | ||||
|       { | ||||
|         test: /\.m?js$/, | ||||
|         exclude: /node_modules/, | ||||
|         use: { | ||||
|           loader: 'babel-loader', | ||||
|           options: { | ||||
|             presets: ['@babel/preset-env'] | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         test: /\.js$/, | ||||
|         enforce: 'pre', | ||||
|         use: ['source-map-loader'] | ||||
|       }, | ||||
|       { | ||||
|         test: require.resolve('jquery'), | ||||
|         loader: 'expose-loader', | ||||
|         options: { | ||||
|           exposes: [ | ||||
|             { | ||||
|               globalName: ['$'], | ||||
|               override: true | ||||
|             }, | ||||
|             { | ||||
|               globalName: ['jQuery'], | ||||
|               override: true | ||||
|             }, | ||||
|             { | ||||
|               globalName: ['window.jQuery'], | ||||
|               override: true | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| 	entry: glob | ||||
| 		.sync("./!(static)/static/webpack/**?(-)index.js") | ||||
| 		.reduce((obj, el) => { | ||||
| 			obj[path.parse(el).name] = `./${el}`; | ||||
| 			return obj; | ||||
| 		}, {}), | ||||
| 	output: { | ||||
| 		filename: "[name].js", | ||||
| 		path: path.resolve(__dirname, "./staticfiles/generated/webpack"), | ||||
| 		clean: true, | ||||
| 	}, | ||||
| 	plugins: [new MiniCssExtractPlugin()], | ||||
| 	optimization: { | ||||
| 		minimizer: [ | ||||
| 			"...", | ||||
| 			new CssMinimizerPlugin({ | ||||
| 				parallel: true, | ||||
| 			}), | ||||
| 			new TerserPlugin({ | ||||
| 				parallel: true, | ||||
| 				terserOptions: { | ||||
| 					mangle: true, | ||||
| 					compress: { | ||||
| 						drop_console: true, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}), | ||||
| 		], | ||||
| 	}, | ||||
| 	module: { | ||||
| 		rules: [ | ||||
| 			{ | ||||
| 				test: /\.css$/, | ||||
| 				sideEffects: true, | ||||
| 				use: [MiniCssExtractPlugin.loader, "css-loader"], | ||||
| 			}, | ||||
| 			{ | ||||
| 				test: /\.(jpe?g|png|gif)$/i, | ||||
| 				type: "asset/resource", | ||||
| 			}, | ||||
| 			{ | ||||
| 				test: /\.m?js$/, | ||||
| 				exclude: /node_modules/, | ||||
| 				use: { | ||||
| 					loader: "babel-loader", | ||||
| 					options: { | ||||
| 						presets: ["@babel/preset-env"], | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			{ | ||||
| 				test: /\.js$/, | ||||
| 				enforce: "pre", | ||||
| 				use: ["source-map-loader"], | ||||
| 			}, | ||||
| 			{ | ||||
| 				test: require.resolve("jquery"), | ||||
| 				loader: "expose-loader", | ||||
| 				options: { | ||||
| 					exposes: [ | ||||
| 						{ | ||||
| 							globalName: ["$"], | ||||
| 							override: true, | ||||
| 						}, | ||||
| 						{ | ||||
| 							globalName: ["jQuery"], | ||||
| 							override: true, | ||||
| 						}, | ||||
| 						{ | ||||
| 							globalName: ["window.jQuery"], | ||||
| 							override: true, | ||||
| 						}, | ||||
| 					], | ||||
| 				}, | ||||
| 			}, | ||||
| 		], | ||||
| 	}, | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user