Monday, 11 March 2013

Daily report cutter - part II


It's always nice to come home after a tired day, sit down, make a tea and do some CoffeeScript to calm yourself down. It's like really your first frozen girlfriend. You do whatever you want and you even can peep under the skirt and it's still stable.


I'm still experimenting the language but it's already obvious how easy to work with a well structured syntax. The old cluttered function macaroni is now a clean (more or less) class collection. I've got some taste from the flexibility of CoffeeScript as well - how you can define the exact same thing in different ways. (Wait, isn't that JavaScript? Ups.)

So I cleaned the daily report cutter and added a little. I had to realize it's tough to make a UI controller. I wanted to use a library but I haven't found anything that supports multiple scrollers on the same controller.

A little aperitif from the hipster colored demo:


The current structure is very raw. We have a Label class that is responsible about the label items and their data:

class Label
  @labels: []
  @width: 100

  html: null

  constructor: (position) ->
    Label.labels.push(this)
    @html = jQuery('<input type="textfield" value="activity"/>')
    jQuery('#label_bar').append(@html)
    jQuery(@html).change -> StageManager.updateReport()
    StageManager.updateElements()

  offsetX: () ->
    jQuery(@html).offset().left

  setOffsetX: (offsetX) ->
    jQuery(@html).css('left', offsetX)

  kill: () ->
    jQuery(@html).remove()
    _this = @
    Label.labels = Label.labels.filter (l) -> l != _this

  text: () ->
    jQuery(@html).val()


The other class is for the divider:

class Divider
  @dividers: []
  @width: 10
  @widthHalf: @width >> 1
  @counter: 0
  @activeDivider: null

  html: null
  count: @counter++

  constructor: (coordX) ->
    dividerWidthHalf = Divider.width >> 1

    Divider.dividers.push(this)

    @html = jQuery('<div class="divider"/>')
    @html.attr('id', 'divider_' + Divider.counter)
    @setOffsetX(coordX - dividerWidthHalf)
    @html.mousedown (e) =>
      @mouseDown(e)
    @html.click (e) =>
      e.stopPropagation()
    @html.dblclick StageManager.doubleClickOnDivider
    jQuery('#time_bar').append(@html)

    Divider.activeDivider = this
    Divider.counter++

  offsetX: () ->
    jQuery(@html).offset().left

  setOffsetX: (offsetX) ->
    @html.css('left', offsetX)

  mouseDown: (e) ->
    Divider.activeDivider = @
    e.stopPropagation()

  getHTMLAttr: (param) ->
    jQuery(@html).attr(param)

  kill: () ->
    _this = @
    Divider.dividers = Divider.dividers.filter (d) -> d != _this
    jQuery(@html).remove()

  index: () ->
    for divider, i in Divider.dividers
      if divider == @
        return i
    -1

  @getByID: (id) ->
    for divider in Divider.dividers
      if divider.getHTMLAttr('id') == id
        return divider
    null

  @sort: () ->
    Divider.dividers.sort (a, b) ->
      jQuery(a.html).offset().left > jQuery(b.html).offset().left


It's a bit more but on the UI it also more in the central of interactions. Having these 2 objects on the scene is not really enough. When adding dividers you want to adjust on the label set also. And the logical solution is a higher level controller - that's the stage manager:

class StageManager
  @updateElements: () ->
    labelWidthHalf = Label.width >> 1
    if Divider.dividers.length == 0
      bar_width_half = jQuery('#label_bar').width() >> 1
      Label.labels[0].setOffsetX(bar_width_half - labelWidthHalf)
    else
      prev_x = 0
      for divider, i in Divider.dividers
        divider_x = divider.offsetX()
        label_x = (divider_x + prev_x) * 0.5 - labelWidthHalf
        Label.labels[i].setOffsetX(label_x)
        prev_x = divider_x
      label_i = Label.labels[i]
      label_i.setOffsetX((jQuery('#label_bar').width() + prev_x) * 0.5 - labelWidthHalf)
    @updateReport()

  @updateReport: ->
    jQuery('#report').html('<ol />')
    for label in Label.labels
      text = label.text()
      jQuery('#report ol').append('<li>' + text + ' ' + label.timeLabel() + '</li>')

  @moveBarMouseMove: (e) ->
    if Divider.activeDivider
      Divider.activeDivider.setOffsetX(e.offsetX - Divider.widthHalf)

  @timeBarMouseUp: () ->
    Divider.activeDivider = null
    Divider.sort()
    StageManager.updateElements()

  @onClickTimeBar: (e) ->
    new Divider(e.offsetX)
    new Label(0)

  @doubleClickOnDivider: (e) ->
    id = jQuery(e.delegateTarget).attr('id')
    divider_to_kill = Divider.getByID(id)

    idx_of_divider = divider_to_kill.index()
    Label.labels[idx_of_divider].kill()

    divider_to_kill.kill()

  @getTimeInterval: () ->
    {
      start: 540,
      end: 1080
    }

  @timeBarWidth: ->
    jQuery('#label_bar').width()


It cares about the high level events, layout of labels and dividers and the app lifecycle. The whole app initialization is pretty simple:

jQuery ->
  jQuery('#time_bar').click StageManager.onClickTimeBar
  jQuery('#time_bar').mouseup StageManager.timeBarMouseUp
  jQuery('#move_layer').mousemove StageManager.moveBarMouseMove
  new Label()


And I'm sure you think it's not the best structure - and I agree. That's the brilliant :) You still can make it better. It's just a bit tricky when you're full with the dinner and want to watch the same adventure time episode again and again. Check out the current source on GitHub.

---

Peter

No comments:

Post a Comment