Conteúdo do cartão de usuário fixo tema

Criei um tema mais elaborado para user-card-contents, adicionando alguns campos personalizados de usuário. Gostaria de tornar o card fixo (sticky), com um botão de fechar — de forma semelhante ao componente “Criar um novo tópico”.

Estou correto ao pensar que precisarei sobrescrever o user-card-contents.js para evitar a chamada que fecha o elemento? Seria possível empacotar isso dentro do tema?

Obrigado!

Ei, Pete. Bem-vindo ao Meta :wave:

A resposta curta é sim. Se suas alterações afetam apenas o front-end, elas podem ser feitas em um tema ou componente.

Você pode elaborar o que quer dizer com “sticky”? Se você quer dizer que deseja que ele role junto com o conteúdo, mas permaneça na mesma posição, isso seria uma alteração de CSS. Além disso, a alteração deve ser incluída tanto no desktop quanto no mobile?

Você pode descrever qual chamada específica está se referindo? (como ao clicar, ao rolar, etc.)

O markup do botão de fechar precisaria ser adicionado ao template Handlebars do cartão de usuário. A lógica para lidar com a ação quando o botão for clicado precisaria ser adicionada ao arquivo .js do componente.

Uma sobrescrita completa pode não ser necessária; existem alguns ganchos (hooks) que você pode usar para sobrescrever métodos específicos em uma classe. Posso compartilhar mais sobre isso se você descrever um pouco mais o que deseja fazer.

Obrigado pela boas-vindas, Joe!

O que quero fazer é impedir que o manipulador de eventos clickOutsideEventName do mixin card-contents-base.js feche o card no desktop. Em vez disso, gostaria de forçar os usuários a clicar em um botão para fechá-lo. Provavelmente precisarei fazer algo diferente para o mobile.

Consegui fazer esse template Handlebars funcionar, agora é resolver o .js :slight_smile:

TL;DR Acredito que é disso que você precisa

Theme JS

api.modifyClass("component:user-card-contents", {
  didInsertElement() {
    this._super(...arguments);
    $("html").off(this.clickOutsideEventName);
  },
  actions: {
    closeCard() {
      this._close();
    }
  }
});

e depois adicione isso ao seu template em algum lugar

{{d-button
  class="btn-flat"
  action=(action "closeCard")
  icon="times"
}}

A versão longa

Você provavelmente já sabe a maior parte disso, já que trabalhou no seu tema, mas vou tentar manter um pouco mais detalhado para um público mais amplo.

A primeira coisa que eu faria é pesquisar localmente ou no Github. No Github, você encontraria algo como isso. Na maioria dos casos, um termo de pesquisa terá mais de um resultado e você terá que ser mais específico ou examinar manualmente o resultado para encontrar algo próximo do que deseja.

Então, agora chegamos a este arquivo

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Este arquivo é um Mixin. Por que menciono isso? Porque você precisa estar ciente de que os mixins podem ser compartilhados em vários lugares diferentes. Neste caso, é usado tanto para cartões de usuário quanto para cartões de grupo. Portanto, as alterações que você fizer aqui afetarão ambos.

Se você pesquisar no arquivo, verá que clickOutsideEventName é definido pela primeira vez aqui

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

então passado adiante aqui como uma propriedade

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

e finalmente consumido aqui

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Legal, mas o que tudo isso significa? Bem, se você olhar para onde todo esse código é adicionado, notará que está dentro de didInsertElement

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Os guias do Ember afirmam

O Ember garante que, no momento em que didInsertElement() é chamado:

  1. O elemento do componente foi criado e inserido no DOM.
  2. O elemento do componente é acessível através da propriedade this.element do componente.

Por que precisamos disso? Porque precisamos de um manipulador de mousedown diferente para cartões de usuário e cartões de grupo. Se voltarmos um pouco, agora notará que o ID do elemento é usado no clickOutsideEventName

O que, como discutimos acima, é então passado adiante como uma propriedade.

Agora, vamos passar para como tudo isso se relaciona com o que você está fazendo.

Você está tentando evitar que os cartões sejam fechados quando o usuário clica fora deles. Então, vamos tentar descobrir uma maneira de fazer isso. Se você se lembra, discutimos como clickOutsideEventName acaba sendo consumido aqui

Em resumo, isso adiciona um manipulador de mousedown ao elemento HTML quando um cartão é inserido (na primeira visualização da página). Em seguida, verificamos o alvo do evento de mousedown. Se o alvo estiver em algum lugar no cartão, saímos. Se estiver fora do cartão, fechamos chamando this._close()

_close() é definido aqui

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

E é isso que você precisará chamar ao adicionar seu botão de fechar — mas voltaremos a isso mais tarde.

Agora, o objetivo é remover esse manipulador de mousedown se você quiser que cliques fora do cartão não o fechem. Então, como fazemos isso? Bem, precisaremos modificar didInsertElement() e é assim que isso pode ser feito.

Os temas do Discourse têm acesso à API de plugin, que contém muitos métodos que você pode usar. Há um pouco mais de detalhes sobre os mais comumente usados aqui

Se você se lembrar, o arquivo com o qual estamos trabalhando é um Mixin. Um Mixin é uma classe Ember. Então, o método que vamos usar é modifyClass

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/lib/plugin-api.js#L121-L124

Quando você usa modifyClass, pode adicionar, modificar ou substituir completamente um método de classe. Vamos nos concentrar em modificar um método, já que é isso que você quer fazer.

Queremos modificar didInsertElement(), então podemos fazer algo assim

api.modifyClass('mixin:card-contents-base', {
  didInsertElement() {
    console.log("foo");
  }
});

e tentar…

bem, isso não funcionou.

por que isso? Bem, acontece que o método modifyClass atualmente não suporta a modificação de Mixins (pelo menos nos testes que fiz). Vou anotar para descobrir por que isso acontece e verificar se podemos corrigir isso. Por enquanto, vamos voltar ao que você quer fazer.

Bem, não podemos modificar Mixins, então acho que estamos presos, certo? Não. Vamos cavar um pouco mais fundo.

Como mencionei antes, esse Mixin é usado tanto pelo componente de usuário user-card-contents quanto pelo group-card-contents (novamente, porque os Mixins são projetados para tornar o código reutilizável)

Então, vamos olhar para o componente user-card-contents aqui

discourse/app/assets/javascripts/discourse/app/components/user-card-contents.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Se você ler com atenção, notará que primeiro importamos o Mixin que discutimos acima aqui

e depois criamos um novo componente Ember e passamos o Mixin para ele.

O que isso significa? Significa que didInsertElement() para user-card-contents é realmente herdado do Mixin. O mesmo pode ser dito sobre group-card-contents.

Onde isso nos deixa? Bem, há boas e más notícias. A boa notícia é que, se você quiser fazer alterações no user-card-contents sem afetar o group-card-contents, então pode! A má notícia é que, se você quiser que suas alterações se apliquem a ambos, terá que duplicar algum código.

Vamos voltar a modifyClass e tentar novamente com user-card-contents. Então, algo assim:

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    console.log("foo");
  }
});

e ver o que acontece…

voalá :tada:

A alteração é registrada e podemos vê-la no console, mas ainda não chegamos lá.

Então, agora que podemos modificar didInsertElement(), vamos tentar voltar ao que você está tentando fazer. Se você se lembra, o manipulador de mousedown é definido em didInsertElement aqui

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Então, o que podemos fazer a respeito? Bem, é tão simples quanto isso

$("html").off(clickOutsideEventName)

Isso vai funcionar? Não. Se você fizer isso

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    $("html").off(clickOutsideEventName)
  }
});

Você acabará com algo quebrado. Por que isso? Porque isso não é uma modificação do método. É uma substituição completa do método principal didInsertElement() para aquele componente. Então, nenhum do código principal é realmente aplicado se você fizer isso.

Como corrigimos isso? Bem, acontece que o Ember tem algo chamado this._super(...arguments)

O que isso faz? Permite que você anexe ou antependa código além do que o método da classe já tem. Por exemplo, se você fizer isso

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // código que você quer adicionar
    $("html").off(clickOutsideEventName);
    // código do núcleo
    this._super(...arguments);
  }
});

então o código que você quer adicionar será executado antes de qualquer outra coisa aqui

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

No entanto, se você fizer isso…

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // código do núcleo
    this._super(...arguments);
    // código que você quer adicionar
    $("html").off(clickOutsideEventName);
  }
});

então seu código será executado após tudo o que roda aqui

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Essa é uma ótima maneira de manter seu tema resiliente a alterações no núcleo, já que tudo no núcleo roda primeiro, e então seu código roda depois. Então, vamos tentar isso novamente e ver o que acontece.

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // código do núcleo
    this._super(...arguments);
    // código que você quer adicionar
    $("html").off(clickOutsideEventName);
  }
});

e…

Por que isso está acontecendo? É por causa de um contexto de código diferente. No arquivo do componente Ember, clickOutsideEventName já está definido no momento em que é consumido. Seu tema está em um arquivo diferente, então clickOutsideEventName não está definido lá.

Como corrigimos isso? Lembra disso?

clickOutsideEventName é uma propriedade do componente, então se você usar this.clickOutsideEventName, deve funcionar. Vamos tentar isso.

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // código do núcleo
    this._super(...arguments);
    // código que você quer adicionar
    $("html").off(this.clickOutsideEventName);
  },
});

E realmente funciona :tada:

Os cartões de usuário agora abrem sem erros e clicar em qualquer lugar fora não faz nada.

Você pode fazer exatamente a mesma coisa para cartões de grupo assim

api.modifyClass('component:group-card-contents', {
  didInsertElement() {
    // código do núcleo
    this._super(...arguments);
    // código que você quer adicionar
    $("html").off(this.clickOutsideEventName);
  },
});

A única coisa que resta é conectar esse botão de fechar ao método _close() que discutimos anteriormente e há três etapas para isso.

  1. adicionar uma ação closeCard (você pode nomear como quiser)
  2. adicionar um botão ao template do cartão de usuário
  3. chamar essa ação quando o botão for clicado.

Vou me ater aos cartões de usuário por simplicidade. Então, adicionamos isso (junto com o que já discutimos)

api.modifyClass("component:user-card-contents", {
  didInsertElement() {
    this._super(...arguments);
    $("html").off(this.clickOutsideEventName);
  },
  // coisas novas
  actions: {
    closeCard() {
      this._close();
    }
  }
});

tudo o que isso faz é chamar o método principal _close() sempre que a ação personalizada closeCard é acionada.

Em seguida, precisamos adicionar um botão ao template do cartão de usuário ou algo assim

{{d-button
  class="btn-flat"
  action=(action "closeCard")
  icon="times"
}}

Meus resultados aproximados são assim

E, claro, você pode fazer algo semelhante para cartões de grupo, como mencionei acima.

Vou deixar a implementação do cartão de grupo e móvel como exercício para você, já que você essencialmente usaria exatamente os mesmos conceitos que discutimos acima se quiser fazer alterações neles, mas por favor, me avise se encontrar algum problema.

Muito obrigado, isso resultou em um ótimo tutorial! Agradeço muito. Eu não tinha percebido sobre a API de Plugins.

Uma coisa que não ficou imediatamente clara acima foi envolver as alterações de JS do tema em tags de script e colocá-las no common/head_tag.html do plugin:

<script type="text/discourse-plugin" version="0.2">
</script>

Por pura curiosidade, o número da versão na tag importa aqui? E é sempre melhor colocá-los no head_tag.html em vez do header.html, ou não importa realmente?

Obrigado!

@Johani Posso criar outro tópico para isso, se preferir, mas estou tentando localizar o controlador user-card do lado do cliente que:

  • carrega o user-card no primeiro clique
  • e redireciona para o user-summary no segundo clique

Meu objetivo é remover a funcionalidade do segundo clique. Obrigado!