Refatorando Minical: uma xícara de Coffeescript

Na semana passada, eu fiz uma reescrita e limpeza bem completa do Minical, meu plugin jQuery datepicker. Eu o abri originalmente apenas para adicionar um pequeno recurso, mas rapidamente me desviei de como a base de código era ruim e quão melhor eu poderia escrevê-la agora (nota: isso acontece com tudo que eu já codifiquei ou projetei, e é completamente normal )

Então, acabei reescrevendo muito mais do que caberia em uma postagem de blog – mas aqui está um resumo de como reescrevi um método específico.

showCalendar: (e) ->
mc
= if e then $(e.target).data("minical") else @
$other_cals
= $("[id^='minical_calendar']").not(mc.$cal)
$other_cals
.data("minical").hideCalendar() if $other_cals.length
return true if mc.$cal.is(":visible") or mc.$el.is(":disabled")
offset
= if mc.align_to_trigger then mc.$trigger[mc.offset_method]() else mc.$el[mc.offset_method]()
height
= if mc.align_to_trigger then mc.$trigger.outerHeight() else mc.$el.outerHeight()
position
=
left
: "#{offset.left + mc.offset.x}px",
top
: "#{height + offset.top + mc.offset.y}px"
mc
.render().css(position).show()
overlap
= mc.$cal.width() + mc.$cal[mc.offset_method]().left - $(window).width()
if overlap > 0
mc
.$cal.css("left", offset.left - overlap - 10)
mc
.attachCalendarKeyEvents()

Esse era o showCalendarmétodo. É denso e desconcertante – claramente escrito por meu irmão gêmeo malvado de 2011, enquanto ele gargalhava malevolamente das profundezas de sua ilha-fortaleza vulcânica. showCalendartem uma tonelada de responsabilidades:

  • use condicionalmente um thisou o armazenamento de dados do destino do evento para sua thisreferência (porque às vezes é um manipulador)
  • ocultar outros calendários na página
  • resgate se o calendário já estiver sendo exibido ou a entrada de texto estiver desativada
  • posicionar-se
  • reconstruir o elemento do calendário
  • ajustar para sobreposição
  • anexar eventos

Suspiro. Para piorar a situação, ele estava sendo chamado como um manipulador de eventos …

@$el.on("focus.minical click.minical", @showCalendar)

… diretamente, de dentro de uma função de manipulador keydown …

preventKeystroke: (e) ->
mc
= @
if mc.$cal.is(":visible") then return true
key
= e.which
keys
=
9: -> true # tab
13: -> # enter
mc
.showCalendar()
false

… e para reposicionar o calendário no redimensionamento da janela.

if @move_on_resize
$
(window).resize(() ->
$cal
= $(".minical:visible")
$cal
.length && $cal.hide().data("minical").showCalendar()
)

São muitos lugares! Há implicações de desempenho aqui (reconstruir todo o calendário sempre que a janela é redimensionada é grosseiro) e o código é simplesmente confuso. Vamos consertar todas as coisas!

Em primeiro lugar, o posicionamento do calendário deve ser um método próprio. Vamos abstrair isso e referenciá-lo diretamente e, enquanto estamos nisso, vamos condensar o código de ajuste de sobreposição lá também. Ainda é prolixo, mas ei, o posicionamento é complicado. Pelo menos está em sua pequena área agora.

positionCalendar: ->
offset
= if @align_to_trigger then @$trigger[@offset_method]() else @$el[@offset_method]()
height
= if @align_to_trigger then @$trigger.outerHeight() else @$el.outerHeight()
position
=
left
: "#{offset.left + @offset.x}px",
top
: "#{height + offset.top + @offset.y}px"
@$cal.css(position)
overlap
= @$cal.width() + @$cal[@offset_method]().left - $(window).width()
if overlap > 0
@$cal.css("left", offset.left - overlap - 10)
@$cal

E faremos referência a ISSO em nosso evento de redimensionamento e nosso método showCalendar.

showCalendar: (e) ->
mc
= if e then $(e.target).data("minical") else @
$other_cals
= $("[id^='minical_calendar']").not(mc.$cal)
$other_cals
.data("minical").hideCalendar() if $other_cals.length
return true if mc.$cal.is(":visible") or mc.$el.is(":disabled")
mc
.render()
@positionCalendar().show()
mc
.attachCalendarKeyEvents()
if @move_on_resize
$
(window).on('resize.minical', $.proxy(@positionCalendar, @))

Ok, isso já está muito melhor. showCalendaré muito mais curto e está sendo chamado uma vez a menos. Mas essa redefinição condicional de thispoderia ser feita de algumas maneiras diferentes que são muito mais claras. Fará muito mais sentido separar a funcionalidade do manipulador de eventos em seu próprio método. Faremos isso definindo um evento que chama showCalendar no contexto adequado.show.minical

@$cal.on("show.minical", $.proxy(@showCalendar, @))

Agora, todas as outras chamadas para showCalendar podem, em vez disso, acionar o evento no elemento e, por sua vez, showCalendarsó está sendo chamado em um único lugar.

Além disso, se fizermos o mesmo para o evento, podemos reduzir a funcionalidade “ocultar outros calendários” a uma linha, portanto, essas linhas de localização / dados / método …hide.minical

$other_cals = $("[id^='minical_calendar']").not(mc.$cal)
$other_cals
.data("minical").hideCalendar() if $other_cals.length

… se tornar um único gatilho de evento.

$(".minical").not(@$cal).trigger('hide.minical')

Ei, nosso showCalendarmétodo é quase sensato agora:

showCalendar: ->
$
(".minical").not(@$cal).trigger('hide.minical')
return if @$cal.is(":visible") or @$el.is(":disabled")
@render()
@positionCalendar().show()
@attachCalendarKeyEvents()

No entanto, ainda há um grande problema: o calendário é refeito incondicionalmente toda vez que é exibido. Poderíamos adicionar lógica para showCalendarcombater isso, mas não acho que showCalendar deva se preocupar se o calendário precisa ser redesenhado ou não. Afinal, é apenas responsável por mostrar o calendário.

Para corrigir isso, acabei reescrevendo o rendermétodo e várias outras associações de eventos. Isso está fora do escopo desta postagem do blog, mas basta dizer que acabei com um novo highlightDaymétodo que é responsável por saber quando redesenhar:

highlightDay: (date) ->

# try and find the day to highlight
$td
= @$cal.find(".#{date_tools.getDayClass(date)}")

# bail if the day is illegal
return if $td.hasClass("minical_disabled")
return if @to and date > @to
return if @from and date < @from

# rerender the proper month and call itself again if the day isn't found
if !$td.length
@render(date)
@highlightDay(date)
return

# highlight the day
klass
= "minical_highlighted"
@$cal.find(".#{klass}").removeClass(klass)
$td
.addClass(klass)

Então aqui está a encarnação atual de showCalendar. ( selected_dayé uma variável interna registrando qual dia foi escolhido e escrito na entrada.)

showCalendar: (e) ->
$
(".minical").not(@$cal).trigger('hide.minical')
return if @$cal.is(":visible") or @$el.is(":disabled")
@highlightDay(@selected_day)
@positionCalendar().show()
@attachCalendarEvents()
e
.preventDefault()

showCalendaragora tem menos da metade do tamanho original. Ele não precisa saber se ou quando o calendário está sendo redesenhado, ou mesmo se o dia em que deveria ser exibido é legal. É apenas responsável por destacar o elemento selecionado (se houver) e posicionar o elemento do calendário. Além disso, a highlightDayimplementação garante que o calendário seja renderizado apenas quando for necessário alternar meses. E há apenas uma condicional. Alívio!

Você pode verificar mais sobre o Minical no site do Minical Github . No geral, o processo de reescrita resultou em uma reescrita de cerca de 60% do plugin, com um aumento drástico na legibilidade. Tenho certeza de que o meu futuro não será tão louco no presente como o presente estava no passado.

(postado originalmente no Hashrocket Blog )