From deda2b4055e331f42582cbaa88b5c6fb11b88ab6 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 15 Oct 2024 22:06:22 +0200 Subject: [PATCH] Replace selec2 with tom-select --- core/static/core/style.scss | 212 +++++++++++++++-- core/static/webpack/ajax-select-index.ts | 74 ++++++ core/static/webpack/jquery-index.js | 3 - core/static/webpack/utils/select2.ts | 286 ----------------------- core/templates/core/base.jinja | 1 + package-lock.json | 48 ++-- package.json | 5 +- sas/static/webpack/sas/viewer-index.ts | 44 ++-- sas/templates/sas/picture.jinja | 6 +- 9 files changed, 321 insertions(+), 358 deletions(-) create mode 100644 core/static/webpack/ajax-select-index.ts delete mode 100644 core/static/webpack/utils/select2.ts diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 82891031..d658d957 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -28,6 +28,7 @@ input[type="file"] { font-size: 1.2em; border-radius: 5px; color: black; + &:hover { background: hsl(0, 0%, 83%); } @@ -63,6 +64,7 @@ textarea[type="text"], border-radius: 5px; max-width: 95%; } + textarea { border: none; text-decoration: none; @@ -72,6 +74,7 @@ textarea { border-radius: 5px; font-family: sans-serif; } + select { border: none; text-decoration: none; @@ -85,9 +88,11 @@ select { a:not(.button) { text-decoration: none; color: $primary-dark-color; + &:hover { color: $primary-light-color; } + &:active { color: $primary-color; } @@ -116,7 +121,9 @@ a:not(.button) { } @keyframes rotate { - 100% { transform: rotate(360deg); } + 100% { + transform: rotate(360deg); + } } .ib { @@ -143,11 +150,13 @@ a:not(.button) { .collapse-header-icon { transition: all ease-in-out 150ms; + &.reverse { transform: rotate(180deg); } } } + .collapse-body { padding: 10px; } @@ -202,9 +211,11 @@ a:not(.button) { font-size: 0.9em; margin: 0.2em; border-radius: 0.6em; + .markdown { margin: 0.5em; } + &:before { font-family: FontAwesome; font-size: 4em; @@ -212,15 +223,19 @@ a:not(.button) { margin: 0.2em; } } + #info_box { background: $primary-neutral-light-color; + &:before { content: "\f05a"; color: hsl(210, 100%, 56%); } } + #alert_box { background: $second-color; + &:before { content: "\f06a"; color: $white-color; @@ -240,7 +255,8 @@ a:not(.button) { #page { width: 90%; margin: 20px auto 0; -/*---------------------------------NAV---------------------------------*/ + + /*---------------------------------NAV---------------------------------*/ .btn { font-size: 15px; font-weight: normal; @@ -252,9 +268,11 @@ a:not(.button) { &.btn-blue { background-color: $deepblue; + &:not(:disabled):hover { background-color: darken($deepblue, 10%); } + &:disabled { background-color: rgba(70, 90, 126, 0.4); } @@ -262,9 +280,11 @@ a:not(.button) { &.btn-grey { background-color: grey; + &:not(:disabled):hover { background-color: darken(gray, 15%); } + &:disabled { background-color: lighten(gray, 15%); } @@ -273,9 +293,11 @@ a:not(.button) { &.btn-red { background-color: #fc8181; color: black; + &:not(:disabled):hover { background-color: darken(#fc8181, 15%); } + &:disabled { background-color: lighten(#fc8181, 15%); color: grey; @@ -287,12 +309,13 @@ a:not(.button) { } } -/*--------------------------------CONTENT------------------------------*/ + /*--------------------------------CONTENT------------------------------*/ #quick_notif { width: 100%; margin: 0 auto; list-style-type: none; background: $second-color; + li { padding: 10px; } @@ -350,6 +373,7 @@ a:not(.button) { .tool_bar { overflow: auto; padding: 4px; + .tools { display: flex; flex-wrap: wrap; @@ -358,6 +382,7 @@ a:not(.button) { padding: 5px; border-radius: 6px; text-align: center; + a { padding: 7px; display: inline-block; @@ -367,11 +392,13 @@ a:not(.button) { flex: 1; flex-wrap: nowrap; white-space: nowrap; + &.selected_tab { background: $primary-color; color: $white-color; border-radius: 6px; } + &:hover { background: $primary-color; color: $white-color; @@ -381,7 +408,7 @@ a:not(.button) { } } -/*---------------------------------NEWS--------------------------------*/ + /*---------------------------------NEWS--------------------------------*/ #news { display: flex; @@ -394,17 +421,21 @@ a:not(.button) { margin: 0; vertical-align: top; } + #news_admin { margin-bottom: 1em; } + #right_column { flex: 20%; float: right; margin: 0.2em; } + #left_column { flex: 79%; margin: 0.2em; + h3 { background: $second-color; box-shadow: $shadow-color 1px 1px 1px; @@ -412,19 +443,22 @@ a:not(.button) { margin: 0 0 0.5em 0; text-transform: uppercase; font-size: 1.1em; + &:not(:first-of-type) { margin: 2em 0 1em 0; } } } + @media screen and (max-width: $small-devices) { + #left_column, #right_column { flex: 100%; } } -/* AGENDA/BIRTHDAYS */ + /* AGENDA/BIRTHDAYS */ #agenda, #birthdays { display: block; @@ -432,6 +466,7 @@ a:not(.button) { background: white; font-size: 70%; margin-bottom: 1em; + #agenda_title, #birthdays_title { margin: 0; @@ -444,39 +479,48 @@ a:not(.button) { text-transform: uppercase; background: $second-color; } + #agenda_content { overflow: auto; box-shadow: $shadow-color 1px 1px 1px; height: 20em; } + #agenda_content, #birthdays_content { .agenda_item { padding: 0.5em; margin-bottom: 0.5em; + &:nth-of-type(even) { background: $secondary-neutral-light-color; } + .agenda_time { font-size: 90%; color: grey; } + .agenda_item_content { p { margin-top: 0.2em; } } } + ul.birthdays_year { margin: 0; list-style-type: none; font-weight: bold; - > li { + + >li { padding: 0.5em; + &:nth-child(even) { background: $secondary-neutral-light-color; } } + ul { margin: 0; margin-left: 1em; @@ -487,13 +531,15 @@ a:not(.button) { } } } -/* END AGENDA/BIRTHDAYS */ -/* EVENTS TODAY AND NEXT FEW DAYS */ + /* END AGENDA/BIRTHDAYS */ + + /* EVENTS TODAY AND NEXT FEW DAYS */ .news_events_group { box-shadow: $shadow-color 1px 1px 1px; margin-left: 1em; margin-bottom: 0.5em; + .news_events_group_date { display: table-cell; padding: 0.6em; @@ -509,33 +555,42 @@ a:not(.button) { div { margin: 0 auto; + .day { font-size: 1.5em; } } } + .news_events_group_items { display: table-cell; width: 100%; + .news_event:nth-of-type(odd) { background: white; } + .news_event:nth-of-type(even) { background: $primary-neutral-light-color; } + .news_event { display: block; padding: 0.4em; + &:not(:last-child) { border-bottom: 1px solid grey; } + div { margin: 0.2em; } + h4 { margin-top: 1em; text-transform: uppercase; } + .club_logo { float: left; min-width: 7em; @@ -543,6 +598,7 @@ a:not(.button) { margin: 0; margin-right: 1em; margin-top: 0.8em; + img { max-height: 6em; max-width: 8em; @@ -550,16 +606,21 @@ a:not(.button) { margin: 0 auto; } } + .news_date { font-size: 100%; } + .news_content { clear: left; + .button_bar { text-align: right; + .fb { color: $faceblue; } + .twitter { color: $twitblue; } @@ -568,26 +629,30 @@ a:not(.button) { } } } -/* END EVENTS TODAY AND NEXT FEW DAYS */ -/* COMING SOON */ + /* END EVENTS TODAY AND NEXT FEW DAYS */ + + /* COMING SOON */ .news_coming_soon { display: list-item; list-style-type: square; list-style-position: inside; margin-left: 1em; padding-left: 0; + a { font-weight: bold; text-transform: uppercase; } + .news_date { font-size: 0.9em; } } -/* END COMING SOON */ -/* NOTICES */ + /* END COMING SOON */ + + /* NOTICES */ .news_notice { margin: 0 0 1em 1em; padding: 0.4em; @@ -595,16 +660,19 @@ a:not(.button) { background: $secondary-neutral-light-color; box-shadow: $shadow-color 0 0 2px; border-radius: 18px 5px 18px 5px; + h4 { margin: 0; } + .news_content { margin-left: 1em; } } -/* END NOTICES */ -/* CALLS */ + /* END NOTICES */ + + /* CALLS */ .news_call { margin: 0 0 1em 1em; padding: 0.4em; @@ -612,21 +680,26 @@ a:not(.button) { background: $secondary-neutral-light-color; border: 1px solid grey; box-shadow: $shadow-color 1px 1px 1px; + h4 { margin: 0; } + .news_date { font-size: 0.9em; } + .news_content { margin-left: 1em; } } -/* END CALLS */ + + /* END CALLS */ .news_empty { margin-left: 1em; } + .news_date { color: grey; } @@ -640,8 +713,8 @@ a:not(.button) { } -.select2 { - margin: 10px 0!important; +.tomselected { + margin: 10px 0 !important; max-width: 100%; min-width: 100%; @@ -657,7 +730,9 @@ a:not(.button) { color: black; } } -.select2-results { + +.ts-dropdown { + .select-item { display: flex; flex-direction: row; @@ -673,16 +748,39 @@ a:not(.button) { } } +.ts-control { + + .item { + .fa-times { + margin-left: 5px; + margin-right: 5px; + } + + cursor: pointer; + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + display: inline-block; + margin-left: 5px; + margin-top: 5px; + margin-bottom: 5px; + padding-right: 10px; + } + +} + #news_details { display: inline-block; margin-top: 20px; padding: 0.4em; width: 80%; background: $white-color; + h4 { margin-top: 1em; text-transform: uppercase; } + .club_logo { display: inline-block; text-align: center; @@ -690,6 +788,7 @@ a:not(.button) { float: left; min-width: 15em; margin: 0; + img { max-height: 15em; max-width: 12em; @@ -698,6 +797,7 @@ a:not(.button) { margin-bottom: 10px; } } + .share_button { border: none; color: white; @@ -709,6 +809,7 @@ a:not(.button) { float: right; display: block; margin-left: 0.3em; + &:hover { color: lightgrey; } @@ -740,26 +841,32 @@ a:not(.button) { #poster_edit, #screen_edit { position: relative; + #title { position: relative; padding: 10px; margin: 10px; border-bottom: 2px solid black; + h3 { display: flex; justify-content: center; align-items: center; } + #links { position: absolute; display: flex; bottom: 5px; + &.left { left: 0; } + &.right { right: 0; } + .link { padding: 5px; padding-left: 20px; @@ -768,27 +875,32 @@ a:not(.button) { border-radius: 20px; background-color: hsl(40, 100%, 50%); color: black; + &:hover { color: black; background-color: hsl(40, 58%, 50%); } + &.delete { background-color: hsl(0, 100%, 40%); } } } } + #posters, #screens { position: relative; display: flex; flex-wrap: wrap; + #no-posters, #no-screens { display: flex; justify-content: center; align-items: center; } + .poster, .screen { min-width: 10%; @@ -800,26 +912,31 @@ a:not(.button) { border-radius: 4px; padding: 10px; background-color: lightgrey; + * { display: flex; justify-content: center; align-items: center; } + .name { padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid whitesmoke; } + .image { flex-grow: 1; position: relative; padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid whitesmoke; + img { max-height: 20vw; max-width: 100%; } + &:hover { &::before { position: absolute; @@ -838,10 +955,12 @@ a:not(.button) { } } } + .dates { padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid whitesmoke; + * { display: flex; justify-content: center; @@ -850,15 +969,18 @@ a:not(.button) { margin-left: 5px; margin-right: 5px; } + .begin, .end { width: 48%; } + .begin { border-right: 1px solid whitesmoke; padding-right: 2%; } } + .edit, .moderate, .slideshow { @@ -866,15 +988,18 @@ a:not(.button) { border-radius: 20px; background-color: hsl(40, 100%, 50%); color: black; + &:hover { color: black; background-color: hsl(40, 58%, 50%); } + &:nth-child(2n) { margin-top: 5px; margin-bottom: 5px; } } + .tooltip { visibility: hidden; width: 120px; @@ -885,23 +1010,28 @@ a:not(.button) { border-radius: 6px; position: absolute; z-index: 10; + ul { margin-left: 0; display: inline-block; + li { display: list-item; list-style-type: none; } } } + &.not_moderated { border: 1px solid red; } + &:hover .tooltip { visibility: visible; } } } + #view { position: fixed; width: 100vw; @@ -915,9 +1045,11 @@ a:not(.button) { visibility: hidden; background-color: rgba(10, 10, 10, 0.9); overflow: hidden; + &.active { visibility: visible; } + #placeholder { width: 80vw; height: 80vh; @@ -926,6 +1058,7 @@ a:not(.button) { align-items: center; top: 0; left: 0; + img { max-width: 100%; max-height: 100%; @@ -940,14 +1073,17 @@ a:not(.button) { tbody { .neg-amount { color: red; + &:before { font-family: FontAwesome; font-size: 1em; content: "\f063"; } } + .pos-amount { color: green; + &:before { font-family: FontAwesome; font-size: 1em; @@ -1014,6 +1150,7 @@ dt { .edit-bar { display: block; margin: 4px; + a { display: inline-block; margin: 4px; @@ -1053,7 +1190,8 @@ th { vertical-align: middle; text-align: center; padding: 5px 10px; - > ul { + + >ul { margin-top: 0; } } @@ -1064,7 +1202,8 @@ td { vertical-align: top; overflow: hidden; text-overflow: ellipsis; - > ul { + + >ul { margin-top: 0; } } @@ -1080,15 +1219,17 @@ thead { color: white; } -tbody > tr { +tbody>tr { &:nth-child(even):not(.highlight) { background: $primary-neutral-light-color; } + &.clickable:hover { cursor: pointer; background: $secondary-neutral-light-color; width: 100%; } + &.highlight { color: $primary-dark-color; font-style: italic; @@ -1148,9 +1289,11 @@ u, margin: 0.2em; height: 100%; background: $secondary-neutral-light-color; + img { max-width: 70%; } + input { background: white; } @@ -1162,10 +1305,12 @@ u, .user_mini_profile { height: 100%; width: 100%; + img { max-width: 100%; max-height: 100%; } + .user_mini_profile_infos { padding: 0.2em; height: 20%; @@ -1173,16 +1318,20 @@ u, flex-wrap: nowrap; justify-content: space-around; font-size: 0.9em; + div { max-height: 100%; } + .user_mini_profile_infos_text { text-align: center; + .user_mini_profile_nick { font-style: italic; } } } + .user_mini_profile_picture { height: 80%; display: flex; @@ -1194,14 +1343,17 @@ u, .mini_profile_link { display: block; text-decoration: none; + span { display: inline-block; width: 50px; vertical-align: middle; } + em { vertical-align: middle; } + img { max-width: 40px; max-height: 60px; @@ -1223,6 +1375,7 @@ u, border: solid 1px red; text-align: center; } + img { width: 500px; } @@ -1232,6 +1385,7 @@ u, .matmat_results { display: flex; flex-wrap: wrap; + .matmat_user { flex-basis: 14em; align-self: flex-start; @@ -1240,10 +1394,12 @@ u, overflow: hidden; border: 1px solid black; box-shadow: $shadow-color 1px 1px 1px; + &:hover { box-shadow: 1px 1px 5px $second-color; } } + .matmat_user a { color: $primary-neutral-dark-color; height: 100%; @@ -1283,6 +1439,7 @@ footer { font-size: 90%; text-align: center; vertical-align: middle; + div { margin: 0.6em 0; color: $white-color; @@ -1292,18 +1449,20 @@ footer { align-items: center; background-color: $primary-neutral-dark-color; box-shadow: $shadow-color 0 0 15px; + a { padding: 0.8em; flex: 1; font-weight: bold; color: $white-color !important; + &:hover { color: $primary-dark-color; } } } - > .version { + >.version { margin-top: 3px; color: rgba(0, 0, 0, 0.3); } @@ -1335,6 +1494,7 @@ label { * { text-align: center; } + img { width: 100px; } @@ -1351,19 +1511,23 @@ label { padding: 2px; display: inline-block; font-size: 0.8em; + span { width: 70px; float: right; } + img { max-width: 50px; max-height: 50px; float: left; } + strong { font-weight: bold; font-size: 1.2em; } + button { vertical-align: middle; } @@ -1380,6 +1544,7 @@ a.ui-button:active, background: $primary-color; border-color: $primary-color; } + .ui-corner-all, .ui-corner-bottom, .ui-corner-right, @@ -1391,10 +1556,11 @@ a.ui-button:active, #club_detail { .club_logo { float: right; + img { display: block; max-height: 10em; max-width: 10em; } } -} +} \ No newline at end of file diff --git a/core/static/webpack/ajax-select-index.ts b/core/static/webpack/ajax-select-index.ts new file mode 100644 index 00000000..aa8c25b9 --- /dev/null +++ b/core/static/webpack/ajax-select-index.ts @@ -0,0 +1,74 @@ +import "tom-select/dist/css/tom-select.css"; +import TomSelect from "tom-select"; +import type { TomItem, TomLoadCallback } from "tom-select/dist/types/types"; +import type { escape_html } from "tom-select/dist/types/utils"; +import { type UserProfileSchema, userSearchUsers } from "#openapi"; + +export class AjaxSelect extends HTMLSelectElement { + widget: TomSelect; + filter?: (items: T[]) => T[]; + + constructor() { + super(); + + window.addEventListener("DOMContentLoaded", () => { + this.loadTomSelect(); + }); + } + + loadTomSelect() { + let maxItems = 1; + + if (this.multiple) { + maxItems = Number.parseInt(this.dataset.max) ?? null; + } + + this.widget = new TomSelect(this, { + hideSelected: true, + maxItems: maxItems, + loadThrottle: Number.parseInt(this.dataset.delay) ?? null, + valueField: "id", + labelField: "display_name", + searchField: ["display_name", "nick_name", "first_name", "last_name"], + placeholder: this.dataset.placeholder ?? "", + load: (query: string, callback: TomLoadCallback) => { + userSearchUsers({ + query: { + search: query, + }, + }).then((response) => { + if (response.data) { + if (this.filter) { + callback(this.filter(response.data.results), []); + } else { + callback(response.data.results, []); + } + return; + } + callback([], []); + }); + }, + render: { + option: (item: UserProfileSchema, sanitize: typeof escape_html) => { + return `
+ ${sanitize(item.display_name)} + ${sanitize(item.display_name)} +
`; + }, + item: (item: UserProfileSchema, sanitize: typeof escape_html) => { + return `${sanitize(item.display_name)}`; + }, + }, + }); + + this.widget.on("item_select", (item: TomItem) => { + this.widget.removeItem(item); + }); + } +} + +window.customElements.define("ajax-select", AjaxSelect, { extends: "select" }); diff --git a/core/static/webpack/jquery-index.js b/core/static/webpack/jquery-index.js index 7c5159fe..569d26e8 100644 --- a/core/static/webpack/jquery-index.js +++ b/core/static/webpack/jquery-index.js @@ -13,9 +13,6 @@ require("jquery-ui/ui/widgets/tabs.js"); require("jquery-ui/themes/base/all.css"); -// We ship select2 here, otherwise it will duplicate jquery everywhere we load it -import "select2"; - /** * Simple wrapper to solve shorten not being able on legacy pages * @param {string} selector to be passed to jQuery diff --git a/core/static/webpack/utils/select2.ts b/core/static/webpack/utils/select2.ts deleted file mode 100644 index b3e5b4d3..00000000 --- a/core/static/webpack/utils/select2.ts +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Builders to use Select2 in our templates. - * - * This comes with two flavours : local data or remote data. - * - * # Local data source - * - * To use local data source, you must define an array - * in your JS code, having the fields `id` and `text`. - * - * ```js - * const data = [ - * {id: 1, text: "foo"}, - * {id: 2, text: "bar"}, - * ]; - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: localDataSource(data) - * })); - * ``` - * - * You can also define a callback that return ids to exclude : - * - * ```js - * const data = [ - * {id: 1, text: "foo"}, - * {id: 2, text: "bar"}, - * {id: 3, text: "to exclude"}, - * ]; - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: localDataSource(data, { - * excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i)) - * }) - * })); - * ``` - * - * # Remote data source - * - * Select2 with remote data sources are similar to those with local - * data, but with some more parameters, like `resultConverter`, - * which takes a callback that must return a `Select2Object`. - * - * ```js - * import { makeUrl } from "#core:utils/api"; - * import {userSearchUsers } from "#openapi" - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - * excluded: () => [1, 2], // exclude users 1 and 2 from the search - * resultConverter: (user: AjaxResponse) => {id: user.id, text: (user.firstName as UserType)} - * }) - * })); - * ``` - * - * # Overrides - * - * Dealing with a select2 may be complex. - * That's why, when defining a select, - * you may add an override parameter, - * in which you can declare any parameter defined in the - * Select2 documentation. - * - * ```js - * import { makeUrl } from "#core:utils/api"; - * import {userSearchUsers } from "#openapi" - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - * resultConverter: (user: AjaxResponse) => {id: user.id, text: (user.firstName as UserType)} - * overrides: { - * delay: 500 - * } - * }) - * })); - * ``` - * - * # Caveats with exclude - * - * With local data source, select2 evaluates the data only once. - * Thus, modify the exclude after the initialisation is a no-op. - * - * With remote data source, the exclude list will be evaluated - * after each api response. - * It makes it possible to bind the data returned by the callback - * to some reactive data, thus making the exclude list dynamic. - * - * # Images - * - * Sometimes, you would like to display an image besides - * the text on the select items. - * In this case, fill the `pictureGetter` option : - * - * ```js - * import { makeUrl } from "#core:utils/api"; - * import {userSearchUsers } from "#openapi" - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - * resultConverter: (user: AjaxResponse) => {id: user.id, text: (user.firstName as UserType)} - * }) - * pictureGetter: (user) => user.profilePict, - * })); - * ``` - * - * # Binding with alpine - * - * You can declare your select2 component in an Alpine data. - * - * ```html - * - *
- * - *

- *

- *
- * - * - * - */ - -import type { - AjaxOptions, - DataFormat, - GroupedDataFormat, - LoadingData, - Options, -} from "select2"; -import "select2/dist/css/select2.css"; - -export interface Select2Object { - id: number; - text: string; -} - -// biome-ignore lint/suspicious/noExplicitAny: You have to do it at some point -export type RemoteResult = any; -export type AjaxResponse = AjaxOptions; - -interface DataSource { - ajax?: AjaxResponse | undefined; - data?: RemoteResult | DataFormat[] | GroupedDataFormat[] | undefined; -} - -interface Select2Options { - element: Element; - /** the data source, built with `localDataSource` or `remoteDataSource` */ - dataSource: DataSource; - excluded?: number[]; - /** A callback to get the picture field from the API response */ - pictureGetter?: (element: LoadingData | DataFormat | GroupedDataFormat) => string; - /** Any other select2 parameter to apply on the config */ - overrides?: Options; -} - -/** - * Create a new select2 with sith presets - */ -export function sithSelect2(options: Select2Options) { - const elem = $(options.element as HTMLInputElement); - return elem.select2({ - theme: elem[0].multiple ? "classic" : "default", - minimumInputLength: 2, - templateResult: selectItemBuilder(options.pictureGetter), - ...options.dataSource, - ...(options.overrides ?? {}), - }); -} - -interface LocalSourceOptions { - excluded: () => number[]; -} - -/** - * Build a data source for a Select2 from a local array - */ -export function localDataSource( - source: Select2Object[] /** Array containing the data */, - options: LocalSourceOptions, -): DataSource { - if (options.excluded) { - const ids = options.excluded(); - return { data: source.filter((i) => !ids.includes(i.id)) }; - } - return { data: source }; -} - -interface RemoteSourceOptions { - /** A callback to the ids to exclude from the search */ - excluded?: () => number[]; - /** A converter for a value coming from the remote api */ - resultConverter?: ((obj: RemoteResult) => DataFormat | GroupedDataFormat) | undefined; - /** Any other select2 parameter to apply on the config */ - overrides?: AjaxOptions; -} - -/** - * Build a data source for a Select2 from a remote url - */ -export function remoteDataSource( - source: string /** url of the endpoint */, - options: RemoteSourceOptions, -): DataSource { - $.ajaxSetup({ - traditional: true, - }); - const params: AjaxOptions = { - url: source, - dataType: "json", - cache: true, - delay: 250, - data: function (params) { - return { - search: params.term, - exclude: [ - ...(this.val() || []).map((i: string) => Number.parseInt(i)), - ...(options.excluded ? options.excluded() : []), - ], - }; - }, - }; - if (options.resultConverter) { - params.processResults = (data) => ({ - results: data.results.map(options.resultConverter), - }); - } - if (options.overrides) { - Object.assign(params, options.overrides); - } - return { ajax: params }; -} - -export function itemFormatter(user: { loading: boolean; text: string }) { - if (user.loading) { - return user.text; - } -} - -/** - * Build a function to display the results - */ -export function selectItemBuilder(pictureGetter?: (item: RemoteResult) => string) { - return (item: RemoteResult) => { - const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null; - const wrapper = document.createElement("div"); - wrapper.classList.add("select-item"); - if (picture) { - const img = document.createElement("img"); - img.src = picture; - img.alt = encodeURI(item.text); - img.onerror = () => { - img.src = "/static/core/img/unknown.jpg"; - }; - wrapper.appendChild(img); - } - const textSpan = document.createElement("span"); - textSpan.classList.add("select-item-text"); - textSpan.appendChild(document.createTextNode(item.text)); - wrapper.appendChild(textSpan); - - return $(wrapper); - }; -} diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 76c6392b..8ce2eb80 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -5,6 +5,7 @@ {% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM + diff --git a/package-lock.json b/package-lock.json index 5a82803d..58c7ece1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,9 @@ "jquery-ui": "^1.14.0", "jquery.shorten": "^1.0.0", "native-file-system-adapter": "^3.0.1", - "select2": "^4.1.0-rc.0", "three": "^0.169.0", - "three-spritetext": "^1.9.0" + "three-spritetext": "^1.9.0", + "tom-select": "^2.3.1" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -37,7 +37,6 @@ "@hey-api/openapi-ts": "^0.53.8", "@types/alpinejs": "^3.13.10", "@types/jquery": "^3.5.31", - "@types/select2": "^4.0.63", "babel-loader": "^9.2.1", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", @@ -2175,6 +2174,19 @@ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" }, + "node_modules/@orchidjs/sifter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz", + "integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==", + "dependencies": { + "@orchidjs/unicode-variants": "^1.0.4" + } + }, + "node_modules/@orchidjs/unicode-variants": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz", + "integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2384,15 +2396,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/@types/select2": { - "version": "4.0.63", - "resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.63.tgz", - "integrity": "sha512-/DXUfPSj3iVTGlRYRYPCFKKSogAGP/j+Z0fIMXbBiBtmmZj0WH7vnfNuckafq9C43KnqPPQW2TI/Rj/vTSGnQQ==", - "dev": true, - "dependencies": { - "@types/jquery": "*" - } - }, "node_modules/@types/sizzle": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", @@ -6132,11 +6135,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/select2": { - "version": "4.1.0-rc.0", - "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz", - "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==" - }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -6614,6 +6612,22 @@ "node": ">=8.0" } }, + "node_modules/tom-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.3.1.tgz", + "integrity": "sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==", + "dependencies": { + "@orchidjs/sifter": "^1.0.3", + "@orchidjs/unicode-variants": "^1.0.4" + }, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tom-select" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", diff --git a/package.json b/package.json index 20df6130..4a1338f7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@hey-api/openapi-ts": "^0.53.8", "@types/alpinejs": "^3.13.10", "@types/jquery": "^3.5.31", - "@types/select2": "^4.0.63", "babel-loader": "^9.2.1", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", @@ -59,8 +58,8 @@ "jquery-ui": "^1.14.0", "jquery.shorten": "^1.0.0", "native-file-system-adapter": "^3.0.1", - "select2": "^4.1.0-rc.0", "three": "^0.169.0", - "three-spritetext": "^1.9.0" + "three-spritetext": "^1.9.0", + "tom-select": "^2.3.1" } } diff --git a/sas/static/webpack/sas/viewer-index.ts b/sas/static/webpack/sas/viewer-index.ts index e8e5f6f4..2222a7a3 100644 --- a/sas/static/webpack/sas/viewer-index.ts +++ b/sas/static/webpack/sas/viewer-index.ts @@ -1,12 +1,7 @@ -import { makeUrl, paginated } from "#core:utils/api"; +import { paginated } from "#core:utils/api"; import { exportToHtml } from "#core:utils/globals"; import { History } from "#core:utils/history"; -import { - type AjaxResponse, - type RemoteResult, - remoteDataSource, - sithSelect2, -} from "#core:utils/select2"; +import type TomSelect from "tom-select"; import { type IdentifiedUserSchema, type PictureSchema, @@ -20,7 +15,6 @@ import { picturesFetchPictures, picturesIdentifyUsers, picturesModeratePicture, - userSearchUsers, usersidentifiedDeleteRelation, } from "#openapi"; @@ -182,20 +176,21 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { query: { album_id: config.albumId }, } as PicturesFetchPicturesData) ).map(PictureWithIdentifications.fromPicture); - this.selector = sithSelect2({ - element: this.$refs.search, - dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - excluded: () => [ - ...(this.currentPicture.identifications || []).map( - (i: IdentifiedUserSchema) => i.user.id, - ), - ], - resultConverter: (obj: AjaxResponse) => { - return { ...obj, text: (obj as UserProfileSchema).display_name }; - }, - }), - pictureGetter: (user: RemoteResult) => user.profile_pict, - }); + this.selector = this.$refs.search; + this.selector.filter = (users: UserProfileSchema[]) => { + const resp: UserProfileSchema[] = []; + const ids = [ + ...(this.currentPicture.identifications || []).map( + (i: IdentifiedUserSchema) => i.user.id, + ), + ]; + for (const user of users) { + if (!ids.includes(user.id)) { + resp.push(user); + } + } + return resp; + }; this.currentPicture = this.pictures.find( (i: PictureSchema) => i.id === config.firstPictureId, ); @@ -302,16 +297,17 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { * Send the identification request and update the list of identified users. */ async submitIdentification(): Promise { + const widget: TomSelect = this.selector.widget; await picturesIdentifyUsers({ path: { // biome-ignore lint/style/useNamingConvention: api is in snake_case picture_id: this.currentPicture.id, }, - body: this.selector.val().map((i: string) => Number.parseInt(i)), + body: widget.items.map((i: string) => Number.parseInt(i)), }); // refresh the identified users list await this.currentPicture.loadIdentifications({ forceReload: true }); - this.selector.empty().trigger("change"); + widget.clear(false); }, /** diff --git a/sas/templates/sas/picture.jinja b/sas/templates/sas/picture.jinja index 915a87c0..30f674ce 100644 --- a/sas/templates/sas/picture.jinja +++ b/sas/templates/sas/picture.jinja @@ -1,11 +1,13 @@ {% extends "core/base.jinja" %} {%- block additional_css -%} + - + {%- endblock -%} {%- block additional_js -%} + {%- endblock -%} @@ -156,7 +158,7 @@
{% trans %}People{% endtrans %}
{% if user.was_subscribed %}
- +
{% endif %}