Componente Tag Reveal nelle liste di argomenti - Espandi/Comprimi tag nelle liste di argomenti

Nota: Prima di pubblicare questo nei componenti del tema, volevo prima ricevere un feedback se questo componente del tema è idoneo o se ci sono problemi importanti.

:warning: Divulgazione: Questo componente del tema è stato pianificato, implementato e testato con l’aiuto di strumenti di codifica AI.

Mi piacerebbe sentire il tuo feedback!


:information_source: Riepilogo Tag Reveal
:eyeglasses: Anteprima Non disponibile…
:hammer_and_wrench: Repository GitHub - jrgong420/discourse-tag-reveal
:question: Guida all’installazione Come installare un tema o un componente del tema
:open_book: Nuovo ai temi di Discourse? Guida per principianti all’uso dei temi di Discourse

Discourse Tag Reveal è un componente del tema leggero che mantiene ordinati gli elenchi di argomenti mostrando solo i primi N tag per argomento e sostituendo il resto con un toggle accessibile “+X tag in più”. Gli utenti possono espandere per vedere tutti i tag e comprimere nuovamente alla vista abbreviata. Funziona immediatamente con l’interfaccia utente standard dei tag di Discourse e non richiede modifiche lato server.

Funzionalità

  • Limite di tag configurabile (predefinito: 5) tramite le impostazioni del tema

  • Toggle stilizzato come un tag, accessibile da tastiera (Invio/Spazio) con attributi ARIA

  • Stringhe localizzate utilizzando themePrefix e discourse-i18n

  • Comportamento sicuro per SPA: reimposta e riapplica la logica al cambio di pagina

  • Supporta lo scrolling infinito tramite MutationObserver

  • CSS minimo; rispetta gli stili dei tag core

  • Nessuna sovrascrittura di template o dipendenze da plugin

Screenshot / Demo

…in arrivo

Installazione e Configurazione

  • Testato con la versione di Discourse: 3.6.0beta1

  • Configura le impostazioni nella scheda Impostazioni del componente:

  • max_tags_visible (intero, predefinito 5): Quanti tag mostrare prima di comprimere

  • toggle_tag_style: Stile visivo del toggle per corrispondere all’aspetto del tag (attualmente è implementato solo lo stile “box”)

  • Ambito: influisce sugli elenchi di argomenti (elenchi di argomenti più recenti, nuovi, non letti e di categoria)

Compatibilità con altri componenti del tema

:warning: Sono stati eseguiti solo test minimi, si prega di testare autonomamente prima di distribuire in produzione

Note

  • Assicurati che la gestione dei tag sia abilitata (Amministratore → Impostazioni → Tag), altrimenti non vedrai alcun effetto

  • Se il tuo sito personalizza pesantemente il CSS dei tag, potresti voler modificare gli stili di .ts-toggle per un allineamento visivo perfetto

Idee per il futuro

Non ho davvero intenzione di implementare altre funzionalità, ma sono felice di accettare PR. Alcune idee per il futuro:

  • Abilita/disabilita per i tag nella vista argomento

  • Controllo granulare per pagine e/o categorie specifiche

2 Mi Piace

Hai chiamato intenzionalmente l’impostazione con lo stesso nome di un’impostazione nel core? Sarei preoccupato per eventuali malintesi.

1 Mi Piace

buona osservazione! L’ho appena modificato…

2 Mi Piace

Sembra molto interessante. Lo proverò nel mio ambiente di sviluppo più tardi dato che non sembra funzionare su Theme Creator (a meno che non stia facendo qualcosa di sbagliato?) :thinking:.

Sembra interessante! Potresti condividere alcuni screenshot o registrazioni dello schermo della funzionalità in azione?

:smiley: Aggiunta una rapida demo video nel primo post, vedi qui:

Non ho nemmeno controllato come inviare/aggiungere il mio componente TC lì… :smiley:
Ma in ogni caso, preferisco raccogliere prima qualche feedback qui, e una volta pronto per essere pubblicato in Theme component, vedrò come aggiungerlo lì.

3 Mi Piace

Theme creator non utilizza lo stile box

Potresti voler usare

more_tags:
  one: "+%{count} altro tag"
  other: "+%{count} altri tag"
1 Mi Piace

ottimo punto. Ho dimenticato di cambiare l’etichetta predefinita in +%{count} in più per mantenerla breve e concisa, è così che la usiamo e manteniamo le cose compatte e pulite.

1 Mi Piace

Ehi,

Questa funzionalità potrebbe essere interessante in alcune situazioni!

A prima vista, ci sono alcune cose da notare:

  • Le impostazioni del tema e le impostazioni del sito non sono le stesse. È necessario recuperare prima il servizio per accedere a max_tags_per_topic, ad esempio: const siteSettings = api.container.lookup(\"service:site-settings\");

  • I controlli aggiuntivi per ottenere il limite non dovrebbero essere necessari; è possibile recuperare il valore direttamente. Probabilmente puoi fare Math.min(settings.max_tags_visible, siteSettings.max_tags_per_topic )

  • Non stai ripristinando la visibilità dei separatori.

  • Potresti voler annullare la registrazione degli eventi

  • Il processo al caricamento iniziale non dovrebbe essere necessario con MutationObserver. Di solito, prima di andare a livello globale, vorresti verificare prima se esiste un modo per ridurre l’ambito attorno all’elemento utilizzando l’API (ad esempio, un plugin outlet).

Fammi controllare se c’è un modo diverso!

1 Mi Piace

Dato che si trova nel file api-initializers, funzionerebbe anche @service siteSettings?

Puoi controllare ora? L’ultimo commit dovrebbe aver risolto i punti affrontati

La versione minima di Discourse 3.6.0 significa che ci vorrà parecchio tempo prima che chiunque possa utilizzarla. Intendevi 3.5.0 o 3.6.0beta1?

Intendevo 3.6.0beta1, quella è la versione con cui l’ho testata…

Lo usi in una classe. Altrimenti non funzionerà.

Allora devi scrivere 3.6.0.beta1, altrimenti nessuno potrà installarlo al momento.

Ho controllato un po’. In effetti, non c’è un modo semplice per ottenere ciò; tuttavia, ho trovato un metodo interessante e semplificato per farlo utilizzando l’API.

  • Utilizza il modello di topic per modificare quali tag visibili verranno generati prima che il template venga generato. Ciò significa nessuna manipolazione del DOM e indipendenza dalle impostazioni. A seconda dello stato (revealTags), restituirà l’elenco originale o uno parziale.

  • Per creare il pulsante di attivazione/disattivazione, utilizza l’API per aggiungere un tag con l’HTML di un pulsante (purtroppo non c’è un plugin outlet qui). L’evento click viene gestito separatamente. Al click, lo stato di attivazione/disattivazione viene aggiornato (revealTags) e viene attivato un re-render dell’elenco dei tag.

Il grande vantaggio di questo metodo è che non devi pasticciare con l’HTML e capire cosa mostrare/nascondere con il CSS, in base ai diversi stili.

chrome_lSKqwYt5Z7

Condivido il mio codice di test qui:

import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";
import { computed } from "@ember/object";

export default apiInitializer((api) => {
  const siteSettings = api.container.lookup("service:site-settings");

  const maxVisibleTags = Math.min(
    settings.max_tags_visible,
    siteSettings.max_tags_per_topic
  );

  let topicModels = {};

  api.modifyClass(
    "model:topic",
    (Superclass) =>
      class extends Superclass {
        revealTags = false;

        init() {
          super.init(...arguments);
          topicModels[this.id] = this;
        }

        @computed("tags")
        get visibleListTags() {
          if (this.revealTags) {
            return super.visibleListTags;
          }
          return super.visibleListTags.slice(0, maxVisibleTags);
        }
      }
  );

  api.addTagsHtmlCallback(
    (topic, params) => {
      if (topic.tags.length <= maxVisibleTags) {
        return "";
      }

      const isExpanded = topic.revealTags;
      const label = isExpanded
        ? i18n(themePrefix("js.tag_reveal.hide"))
        : i18n(themePrefix("js.tag_reveal.more_tags"), {
            count: topic.tags.length - maxVisibleTags,
          });

      return `<a class="reveal-tag-action" role="button" aria-expanded="${isExpanded}">${label}</a>`;
    },
    {
      priority: siteSettings.max_tags_per_topic + 1,
    }
  );

  document.addEventListener("click", (event) => {
    const target = event.target;
    if (!target?.matches(".reveal-tag-action")) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    const element =
      target.closest("[data-topic-id]") ||
      document.querySelector("h1[data-topic-id]");
    const topicId = element?.dataset.topicId;
    if (!topicId) {
      return;
    }

    const topicModel = topicModels[topicId];
    if (!topicModel) {
      return;
    }

    topicModel.revealTags = !topicModel.revealTags;
    topicModel.notifyPropertyChange("tags");
  });
});
.reveal-tag-action {
  background-color: var(--primary-50);
  border: 1px solid var(--primary-200);
  color: var(--primary-800);
  font-size: small;
  padding-inline: 3px;
}

.discourse-tags__tag-separator:has(+ .reveal-tag-action) {
  visibility: hidden;
}

2 Mi Piace

Ciao ragazzi, ho pubblicato un altro aggiornamento e aggiunto funzionalità sperimentali aggiuntive (“tag” in primo piano" che vengono sempre per primi e non vengono calcolati verso la quantità massima + riga di argomenti in primo piano nella visualizzazione dell’elenco degli argomenti), quindi il TC generale sta cambiando un po’ con funzionalità più estese per evidenziare determinati elementi in base alle schede configurate.

@Arkshine grazie per aver condiviso il tuo metodo semplificato, lo apprezzo molto!!! Ha interessato anche la visualizzazione di un singolo argomento, quindi abbiamo aggiunto un’impostazione per abilitare manualmente tale comportamento. L’ho implementato in questo branch:

  • Penso che tu debba rivedere il CSS.

    • probabilmente non dovresti aggiungere discourse-tag al pulsante di attivazione, non è un tag.
    • non usare nemmeno la classe box su di esso, rovina lo stile dell’elenco
    • l’impostazione toggle_tag_style ha solo il valore “box”, forse potresti aggiungere “none”, in modo che si adatti meglio allo stile elenco/puntato.
    • inizia in modo semplice e puoi regolare come desideri
      .reveal-tag-action {
        color: var(--primary-500);
      
        &.-box {
          background-color: var(--primary-50);
          outline: 1px solid var(--primary-200);
          padding-inline: 8px;
        }
      }
      
      /* Nasconde l'ultimo separatore prima del pulsante di attivazione */
      .discourse-tags__tag-separator:has(+ .reveal-tag-action) {
        visibility: hidden;
      }
      

    Bs1rdLFyIU

    Stile box nelle impostazioni del sito e del tema:
    chrome_raXs2Gc1sd

Darò ulteriori feedback sul CSS che inserisci. Per ora è ora di dormire.

1 Mi Piace

in realtà era intenzionale. ad esempio, il plugin di voto degli argomenti utilizza la classe sugli elementi “x voti”.

→ vedi in azione nella categoria Feature - Discourse Meta

Grazie, controllerò!

Per ora ho implementato solo lo stile box poiché è quello che sto usando sulla nostra istanza discourse. Aggiungerò gli stili mancanti in seguito (sono aperto a PR :wink: )

Capisco. Penso che abbia senso se visualizzi le informazioni come un tag, ma qui si tratta di un pulsante per visualizzare altri tag; il contesto è diverso per me. Sta a te; non penso che faccia molta differenza.

Per continuare con il feedback:

  • L’elenco dei tag può essere visualizzato in altri luoghi, come: la pagina delle categorie, le attività dell’utente, ecc. Probabilmente rimuoverei l’impostazione collapse_in_topic_view e ne creerei una nuova con percorsi specifici o la abiliterei ovunque.
    Nel mio codice di test, ho usato qualcosa di simile per ignorare altri percorsi:
JS
    function isAllowedRoute(routeName) {
        const fullRoutesName = [
          "index",
          "userActivity.topics",
          "userActivity.read",
          ...siteSettings.top_menu.split("|").map((item) => `discovery.${item}`),
        ];

        const partialRoutesName = ["topic."];

        if (
          fullRoutesName.includes(routeName) ||
          partialRoutesName.some((partial) => routeName.startsWith(partial))
        ) {
          return true;
        }

        return false;
      }
  • L’iniezione CSS può essere sostituita utilizzando l’API per aggiungere una classe a topic-list-item e a un tag, quindi puoi spostare il CSS in common.css.

Ad esempio:

JS
```js
import { defaultRenderTag } from "discourse/lib/render-tag";

api.registerValueTransformer(
  "topic-list-item-class",
  ({ value, context }) => {
    if (highlightedTagsSet.size === 0) {
      return value;
    }

    if (context.topic?.tags?.some((tag) => highlightedTagsSet.has(tag))) {
      return [...value, `highlighted-tag__${settings.highlighted_style}`];
    }

    return value;
  }
);

api.replaceTagRenderer((tag, params) => {
  if (highlightedTagsSet.has(tag)) {
    params.extraClass = params.extraClass || "";
    params.extraClass += "highlighted";
  }

  return defaultRenderTag(tag, params);
});
```
CSS
/* Nasconde l'ultimo separatore prima del pulsante di rivelazione */
.discourse-tags__tag-separator:has(+ .reveal-tag-action) {
  visibility: hidden;
}

.reveal-tag-action {
  color: var(--primary-500);

  &.-box {
    background-color: var(--primary-50);
    outline: 1px solid var(--primary-200);
    padding-inline: 8px;
  }
}

.latest-topic-list-item,
.topic-list-item {
  .discourse-tag.highlighted {
    color: var(--tertiary);
    border-color: var(--tertiary);
    background: color-mix(in srgb, var(--tertiary) 12%, transparent);
    font-weight: 600;
  }

  &.highlighted-tag {
    &__left-border {
      border-left: 3px solid var(--tertiary);
      background: color-mix(in srgb, var(--tertiary) 6%, transparent);
      transition: box-shadow 160ms ease;

      &:hover {
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
      }
    }

    &__outline {
      outline: 1px solid var(--tertiary);
      outline-offset: -2px;
      border-radius: 7px;
      background: color-mix(in srgb, var(--tertiary) 5%, transparent);
      box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06);
      transition: background-color 160ms ease;
    }

    &__card {
      border-left: 3px solid var(--tertiary);
      background: var(--tertiary-very-low);
      border-radius: var(--border-radius);
      padding-block: var(--space-2);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
      transition: box-shadow 160ms ease;

      &:hover {
        box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1);
      }
    }
  }
}

  ```
  • Non è necessario impostare il percorso corrente da onPageChange, è possibile accedervi dal router.
  • Fai attenzione al case delle lettere dei tag. Hai impostazioni del sito che non impongono il minuscolo, quindi penso che sia meglio non modificare il tag.
  • Per quanto riguarda il ripristino dello stato, probabilmente puoi usare onPageChange.
JS
```js
api.onPageChange((url) => {
    const route = api.container.lookup("service:router").recognize(url);
    if (!isAllowedRoute(route?.name)) {
      return;
    }

    for (const [id, model] of topicModels) {
      if (model && model.revealTags) {
        model.revealTags = false;
        model.notifyPropertyChange("tags");
      }
    }
  });
```
  • Se possibile, sarebbe fantastico aggiungere dei test.

Ecco il codice di test completo (ho apportato altre modifiche minori)

JS
import { computed } from "@ember/object";
import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";
import { defaultRenderTag } from "discourse/lib/render-tag";
import { service } from "@ember/service";

export default apiInitializer((api) => {
  const siteSettings = api.container.lookup("service:site-settings");
  const router = api.container.lookup("service:router");

  const maxVisibleTags = Math.min(
    settings.max_tags_visible,
    siteSettings.max_tags_per_topic
  );

  const highlightedTagsSet = new Set(settings.highlighted_tags.split("|"));
  const topicModels = new Map();

  function isAllowedRoute(routeName) {
    const fullRoutesName = [
      "index",
      "userActivity.topics",
      "userActivity.read",
      "tag.show",
      ...siteSettings.top_menu.split("|").map((item) => `discovery.${item}`),
    ];

    const partialRoutesName = ["topic."];

    if (
      fullRoutesName.includes(routeName) ||
      partialRoutesName.some((partial) => routeName.startsWith(partial))
    ) {
      return true;
    }

    return false;
  }

  api.modifyClass(
    "model:topic",
    (Superclass) =>
      class extends Superclass {
        @service router;

        revealTags = false;

        init() {
          super.init(...arguments);
          topicModels.set(String(this.id), this);
        }

        willDestroy() {
          super.willDestroy(...arguments);
          topicModels.delete(String(this.id));
        }

        @computed("tags")
        get visibleListTags() {
          const baseTags = super.visibleListTags || [];

          if (!isAllowedRoute(this.router.currentRouteName)) {
            return baseTags;
          }

          const highlightedList = [];
          const regularList = [];

          baseTags.forEach((tag) => {
            if (highlightedTagsSet.has(tag)) {
              highlightedList.push(tag);
            } else {
              regularList.push(tag);
            }
          });

          if (this.revealTags) {
            return [...highlightedList, ...regularList];
          }

          return [...highlightedList, ...regularList.slice(0, maxVisibleTags)];
        }
      }
  );

  api.addTagsHtmlCallback(
    (topic) => {
      if (!isAllowedRoute(topic.router.currentRouteName)) {
        return "";
      }

      const allTags = topic.tags || [];
      if (allTags.length === 0) {
        return "";
      }

      const highlightedCount = allTags.filter((tag) =>
        highlightedTagsSet.has(tag)
      ).length;
      const regularCount = allTags.length - highlightedCount;
      const effectiveLimit =
        highlightedCount + Math.min(regularCount, maxVisibleTags);

      // Mostra il toggle solo se ci sono tag nascosti
      if (allTags.length <= effectiveLimit) {
        return "";
      }

      const isExpanded = topic.revealTags;
      const hiddenCount = allTags.length - effectiveLimit;
      const label = isExpanded
        ? i18n(themePrefix("js.tag_reveal.hide"))
        : i18n(themePrefix("js.tag_reveal.more_tags"), {
            count: hiddenCount,
          });

      const classList = ["discourse-tag", "reveal-tag-action"];
      if (settings.toggle_tag_style === "box") {
        classList.push("-box");
      }

      return `<a class="${classList.join(" ")}" role="button" aria-expanded="${isExpanded}">${label}</a>`;
    },
    {
      priority: siteSettings.max_tags_per_topic + 1,
    }
  );

  api.registerValueTransformer(
    "topic-list-item-class",
    ({ value, context }) => {
      if (highlightedTagsSet.size === 0) {
        return value;
      }

      if (context.topic?.tags?.some((tag) => highlightedTagsSet.has(tag))) {
        return [...value, `highlighted-tag__${settings.highlighted_style}`];
      }

      return value;
    }
  );

  api.replaceTagRenderer((tag, params) => {
    let newParams = params;

    if (highlightedTagsSet.has(tag)) {
      newParams = {
        ...params,
        extraClass: [params.extraClass, "highlighted"]
          .filter(Boolean)
          .join(" "),
      };
    }

    return defaultRenderTag(tag, newParams);
  });

  document.addEventListener(
    "click",
    (event) => {
      const target = event.target;
      if (!target?.matches(".reveal-tag-action")) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();

      const element =
        target.closest("[data-topic-id]") ||
        document.querySelector("h1[data-topic-id]");
      const topicId = element?.dataset.topicId;
      if (!topicId) {
        return;
      }

      const topicModel = topicModels.get(topicId);
      if (!topicModel) {
        return;
      }

      topicModel.revealTags = !topicModel.revealTags;
      topicModel.notifyPropertyChange("tags");
    },
    true
  );

  api.onPageChange((url) => {
    const route = api.container.lookup("service:router").recognize(url);
    if (!isAllowedRoute(route?.name)) {
      return;
    }

    for (const [id, model] of topicModels) {
      if (model && model.revealTags) {
        model.revealTags = false;
        model.notifyPropertyChange("tags");
      }
    }
  });
});