Browse Source

Refactoring: Bindings and namespaces improves memory usage and resolved ghost bindings.

* Renamed controllers `@bind` and `@unbind` to `@controllerBind` and `@controllerUnbind` to not overwrite spine's methods to take advantage of spine's binding/unbinding and cleanup features.

* Cleanup of controller namespaces `App.ObserverController` -> `App.ControllerObserver`, `App.ObserverActionRow` -> `App.ControllerObserverActionRow` and `App.WizardModal` -> `App.ControllerWizardModal`

* Moved to named local controller names to have better trace backs/debugging (e. g. `class Index` to `class UserIndex`).

* Splited `app/assets/javascripts/app/controllers/_application_controller.coffee` into separate files under `app/assets/javascripts/app/controllers/_application_controller/*.coffee` to have better trace backs/debugging.

* Moved classes with executed code from `app/assets/javascripts/app/controllers/widget/*.coffee` into `app/assets/javascripts/app/controllers/_plugin/` from now `app/assets/javascripts/app/controllers/widget/*.coffee` will only contain UI widgets like popovers.

* Refactored second argument of `@navigate`. Old version was only `true` to hide `@navigate` redirect from browser back list. Now we introduced params instant of boolean argument. Now we support:
  * `hideCurrentLocationFromHistory: true` -> hide redirect from browser back list (old `true` argument)
  * `emptyEl: true` -> will empty `@el` before redirect to new location
  * `removeEl: true` -> will remove `@el` before redirect to new location
Martin Edenhofer 4 years ago
parent
commit
5ee6418c18

+ 22 - 369
app/assets/javascripts/app/controllers/_application_controller.coffee → app/assets/javascripts/app/controllers/_application_controller/_base.coffee

@@ -2,12 +2,7 @@ class App.Controller extends Spine.Controller
   @include App.LogInclude
   @include App.RenderScreen
 
-  constructor: (params) ->
-
-    # unbind old bindings
-    if params && params.el && params.el.unbind
-      params.el.unbind()
-
+  constructor: ->
     super
 
     # generate controllerId
@@ -30,11 +25,14 @@ class App.Controller extends Spine.Controller
       ajaxId = App.Ajax.request(data)
       @ajaxCalls.push ajaxId
 
-  navigate: (location, hideCurrentLocationFromHistory = false) ->
-    @log 'debug', "navigate to '#{location}', hide from history '#{hideCurrentLocationFromHistory}'"
+  navigate: (location, params = {}) ->
+    @log 'debug', "navigate to '#{location}'"
+    @log 'debug', "navigate hide from history '#{params.hideCurrentLocationFromHistory}'" if params.hideCurrentLocationFromHistory
+    @el.empty() if params.emptyEl
+    @el.remove() if params.removeEl
 
     # hide current location from browser history, allow to use back button in browser
-    if hideCurrentLocationFromHistory
+    if params.hideCurrentLocationFromHistory
       if window.history
         history = App.Config.get('History')
         oldLocation = history[history.length-2]
@@ -45,22 +43,14 @@ class App.Controller extends Spine.Controller
   preventDefault: (e) ->
     e.preventDefault()
 
-  bind: (event, callback) =>
+  controllerBind: (event, callback) =>
     App.Event.bind(
       event
       callback
       @controllerId
     )
 
-  one: (event, callback) =>
-    App.Event.bind(
-      event
-      callback
-      @controllerId
-      true
-    )
-
-  unbind: (event, callback) =>
+  controllerUnbind: (event, callback) =>
     App.Event.unbind(
       event
       callback
@@ -85,6 +75,16 @@ class App.Controller extends Spine.Controller
     App.Interval.clearLevel(@controllerId)
     @abortAjaxCalls()
 
+    # release bindings
+    if @el
+      @el.undelegate()
+      @el.unbind()
+      @el.empty()
+
+    # release spine bindings (see release() of spine.coffee)
+    @unbind()
+    @stopListening()
+
   abortAjaxCalls: =>
     if !@ajaxCalls
       return
@@ -125,11 +125,11 @@ class App.Controller extends Spine.Controller
 
   # add @notify method to create notification
   notify: (data) ->
-    App.Event.trigger 'notify', data
+    App.Event.trigger('notify', data)
 
   # add @notifyDesktop method to create desktop notification
   notifyDesktop: (data) ->
-    App.Event.trigger 'notifyDesktop', data
+    App.Event.trigger('notifyDesktop', data)
 
   # add @navupdate method to update navigation
   navupdate: (url, force = false) ->
@@ -137,17 +137,7 @@ class App.Controller extends Spine.Controller
     # ignore navupdate until #clues are gone
     return if !force && window.location.hash is '#clues'
 
-    App.Event.trigger 'navupdate', url
-
-  # show navigation
-  navShow: ->
-    return if $('#navigation').is(':visible')
-    $('#navigation').removeClass('hide')
-
-  # hide navigation
-  navHide: ->
-    return if !$('#navigation').is(':visible')
-    $('#navigation').addClass('hide')
+    App.Event.trigger('navupdate', url)
 
   updateNavMenu: =>
     delay = ->
@@ -402,340 +392,3 @@ class App.Controller extends Spine.Controller
       return true
 
     throw 'Cant reload page!'
-
-class App.ControllerPermanent extends App.Controller
-  constructor: ->
-    if @requiredPermission
-      @permissionCheckRedirect(@requiredPermission, true)
-
-    super
-
-    @navShow()
-
-class App.ControllerSubContent extends App.Controller
-  constructor: ->
-    if @requiredPermission
-      @permissionCheckRedirect(@requiredPermission)
-
-    super
-
-  show: =>
-    if @genericController && @genericController.show
-      @genericController.show()
-    return if !@header
-    @title @header, true
-
-  hide: =>
-    if @genericController && @genericController.hide
-      @genericController.hide()
-
-class App.ControllerContent extends App.Controller
-  constructor: ->
-    if @requiredPermission
-      @permissionCheckRedirect(@requiredPermission)
-
-    super
-
-    # hide tasks
-    App.TaskManager.hideAll()
-    $('#content').removeClass('hide').removeClass('active')
-    @navShow()
-
-class App.ControllerModal extends App.Controller
-  authenticateRequired: false
-  backdrop: true
-  keyboard: true
-  large: false
-  small: false
-  veryLarge: false
-  head: '?'
-  autoFocusOnFirstInput: true
-  container: null
-  buttonClass: 'btn--success'
-  centerButtons: []
-  leftButtons: []
-  buttonClose: true
-  buttonCancel: false
-  buttonCancelClass: 'btn--text btn--subtle'
-  buttonSubmit: true
-  includeForm: true
-  headPrefix: ''
-  shown: true
-  closeOnAnyClick: false
-  initalFormParams: {}
-  initalFormParamsIgnore: false
-  showTrySupport: false
-  showTryMax: 10
-  showTrydelay: 1000
-
-  events:
-    'submit form':                        'submit'
-    'click .js-submit:not(.is-disabled)': 'submit'
-    'click .js-cancel':                   'cancel'
-    'click .js-close':                    'cancel'
-
-  className: 'modal fade'
-
-  constructor: ->
-    super
-    @showTryCount = 0
-
-    if @authenticateRequired
-      return if !@authenticateCheckRedirect()
-
-    # rerender view, e. g. on langauge change
-    @bind('ui:rerender', =>
-      @update()
-      'modal'
-    )
-    if @shown
-      @render()
-
-  showDelayed: =>
-    delay = =>
-      @showTryCount += 1
-      @render()
-    @delay(delay, @showTrydelay)
-
-  modalAlreadyExists: ->
-    return true if $('.modal').length > 0
-    false
-
-  content: ->
-    'You need to implement a one @content()!'
-
-  update: =>
-    if @message
-      content = App.i18n.translateContent(@message)
-    else if @contentInline
-      content = @contentInline
-    else
-      content = @content()
-    modal = $(App.view('modal')(
-      head:              @head
-      headPrefix:        @headPrefix
-      message:           @message
-      detail:            @detail
-      buttonClose:       @buttonClose
-      buttonCancel:      @buttonCancel
-      buttonCancelClass: @buttonCancelClass
-      buttonSubmit:      @buttonSubmit
-      buttonClass:       @buttonClass
-      centerButtons:     @centerButtons
-      leftButtons:       @leftButtons
-      includeForm:       @includeForm
-    ))
-    modal.find('.modal-body').html(content)
-    if !@initRenderingDone
-      @initRenderingDone = true
-      @html(modal)
-    else
-      @$('.modal-dialog').replaceWith(modal)
-    @post()
-
-  post: ->
-    # nothing
-
-  element: =>
-    @el
-
-  render: =>
-    if @showTrySupport is true && @modalAlreadyExists() && @showTryCount <= @showTryMax
-      @showDelayed()
-      return
-
-    @initalFormParamsIgnore = false
-
-    if @buttonSubmit is true
-      @buttonSubmit = 'Submit'
-    if @buttonCancel is true
-      @buttonCancel = 'Cancel & Go Back'
-
-    @update()
-
-    if @container
-      @el.addClass('modal--local')
-    if @veryLarge
-      @el.addClass('modal--veryLarge')
-    if @large
-      @el.addClass('modal--large')
-    if @small
-      @el.addClass('modal--small')
-
-    @el
-      .on(
-        'show.bs.modal':   @localOnShow
-        'shown.bs.modal':  @localOnShown
-        'hide.bs.modal':   @localOnClose
-        'hidden.bs.modal': @localOnClosed
-        'dismiss.bs.modal': @localOnCancel
-      ).modal(
-        keyboard:  @keyboard
-        show:      true
-        backdrop:  @backdrop
-        container: @container
-      )
-
-    if @closeOnAnyClick
-      @el.on('click', =>
-        @close()
-      )
-
-  close: (e) =>
-    if e
-      e.preventDefault()
-    @initalFormParamsIgnore = true
-    @el.modal('hide')
-
-  formParams: =>
-    if @container
-      return @formParam(@container.find('.modal form'))
-    return @formParam(@$('.modal form'))
-
-  showAlert: (message, suffix = 'danger') ->
-    alert = $('<div>')
-      .addClass("alert alert--#{suffix}")
-      .text(message)
-
-    @$('.modal-alerts-container').html(alert)
-
-  clearAlerts: ->
-    @$('.modal-alerts-container').empty()
-
-  localOnShow: (e) =>
-    @onShow(e)
-
-  onShow: (e) ->
-    # do nothing
-
-  localOnShown: (e) =>
-    @onShown(e)
-
-  onShown: (e) =>
-    if @autoFocusOnFirstInput
-
-      # select generated form
-      form = @$('.form-group').first()
-
-      # if not exists, use whole @el
-      if !form.get(0)
-        form = @el
-
-      # focus first input, select or textarea
-      form.find('input:not([disabled]):not([type="hidden"]):not(".btn"), select:not([disabled]), textarea:not([disabled])').first().focus()
-
-    @initalFormParams = @formParams()
-
-  localOnClose: (e) =>
-    diff = difference(@initalFormParams, @formParams())
-    if @initalFormParamsIgnore is false && !_.isEmpty(diff)
-      if !confirm(App.i18n.translateContent('The form content has been changed. Do you want to close it and lose your changes?'))
-        e.preventDefault()
-        return
-    @onClose(e)
-
-  onClose: ->
-    # do nothing
-
-  localOnClosed: (e) =>
-    @onClosed(e)
-    @el.modal('remove')
-
-  onClosed: (e) ->
-    # do nothing
-
-  localOnCancel: (e) =>
-    @onCancel(e)
-
-  onCancel: (e) ->
-    # do nothing
-
-  cancel: (e) =>
-    @close(e)
-    @onCancel(e)
-
-  onSubmit: (e) ->
-    # do nothing
-
-  submit: (e) =>
-    e.stopPropagation()
-    e.preventDefault()
-    @clearAlerts()
-    @onSubmit(e)
-
-  startLoading: =>
-    @$('.modal-body').addClass('hide')
-    @$('.modal-loader').removeClass('hide')
-
-  stopLoading: =>
-    @$('.modal-body').removeClass('hide')
-    @$('.modal-loader').addClass('hide')
-
-class App.SessionMessage extends App.ControllerModal
-  showTrySupport: true
-
-  onCancel: (e) =>
-    if @forceReload
-      @windowReload(e)
-
-  onClose: (e) =>
-    if @forceReload
-      @windowReload(e)
-
-  onSubmit: (e) =>
-    if @forceReload
-      @windowReload(e)
-    else
-      @close()
-
-class App.UpdateHeader extends App.Controller
-  constructor: ->
-    super
-
-    # subscribe and reload data / fetch new data if triggered
-    @subscribeId = @genericObject.subscribe(@render)
-
-  release: =>
-    App[ @genericObject.constructor.className ].unsubscribe(@subscribeId)
-
-  render: (genericObject) =>
-    @el.find('.page-header h1').html(genericObject.displayName())
-
-
-class App.UpdateTastbar extends App.Controller
-  constructor: ->
-    super
-
-    # subscribe and reload data / fetch new data if triggered
-    @subscribeId = @genericObject.subscribe(@update)
-
-  release: =>
-    App[ @genericObject.constructor.className ].unsubscribe(@subscribeId)
-
-  update: (genericObject) =>
-
-    # update taskbar with new meta data
-    App.TaskManager.touch(@taskKey)
-
-class App.ControllerWidgetPermanent extends App.Controller
-  constructor: (params) ->
-    if params.el
-      params.el.append('<div id="' + params.key + '"></div>')
-      params.el = ("##{params.key}")
-
-    super(params)
-
-class App.ControllerWidgetOnDemand extends App.Controller
-  constructor: (params) ->
-    params.el = $("##{params.key}")
-    super
-
-  element: =>
-    $("##{@key}")
-
-  html: (raw) =>
-
-    # check if parent exists
-    if !$("##{@key}").get(0)
-      $('#app').before("<div id=\"#{@key}\" class=\"#{@className}\"></div>")
-    $("##{@key}").html raw

+ 229 - 0
app/assets/javascripts/app/controllers/_application_controller/_modal.coffee

@@ -0,0 +1,229 @@
+class App.ControllerModal extends App.Controller
+  authenticateRequired: false
+  backdrop: true
+  keyboard: true
+  large: false
+  small: false
+  veryLarge: false
+  head: '?'
+  autoFocusOnFirstInput: true
+  container: null
+  buttonClass: 'btn--success'
+  centerButtons: []
+  leftButtons: []
+  buttonClose: true
+  buttonCancel: false
+  buttonCancelClass: 'btn--text btn--subtle'
+  buttonSubmit: true
+  includeForm: true
+  headPrefix: ''
+  shown: true
+  closeOnAnyClick: false
+  initalFormParams: {}
+  initalFormParamsIgnore: false
+  showTrySupport: false
+  showTryMax: 10
+  showTrydelay: 1000
+
+  events:
+    'submit form':                        'submit'
+    'click .js-submit:not(.is-disabled)': 'submit'
+    'click .js-cancel':                   'cancel'
+    'click .js-close':                    'cancel'
+
+  className: 'modal fade'
+
+  constructor: ->
+    super
+    @showTryCount = 0
+
+    if @authenticateRequired
+      return if !@authenticateCheckRedirect()
+
+    # rerender view, e. g. on langauge change
+    @controllerBind('ui:rerender', =>
+      @update()
+      'modal'
+    )
+    if @shown
+      @render()
+
+  showDelayed: =>
+    delay = =>
+      @showTryCount += 1
+      @render()
+    @delay(delay, @showTrydelay)
+
+  modalAlreadyExists: ->
+    return true if $('.modal').length > 0
+    false
+
+  content: ->
+    'You need to implement a one @content()!'
+
+  update: =>
+    if @message
+      content = App.i18n.translateContent(@message)
+    else if @contentInline
+      content = @contentInline
+    else
+      content = @content()
+    modal = $(App.view('modal')(
+      head:              @head
+      headPrefix:        @headPrefix
+      message:           @message
+      detail:            @detail
+      buttonClose:       @buttonClose
+      buttonCancel:      @buttonCancel
+      buttonCancelClass: @buttonCancelClass
+      buttonSubmit:      @buttonSubmit
+      buttonClass:       @buttonClass
+      centerButtons:     @centerButtons
+      leftButtons:       @leftButtons
+      includeForm:       @includeForm
+    ))
+    modal.find('.modal-body').html(content)
+    if !@initRenderingDone
+      @initRenderingDone = true
+      @html(modal)
+    else
+      @$('.modal-dialog').replaceWith(modal)
+    @post()
+
+  post: ->
+    # nothing
+
+  element: =>
+    @el
+
+  render: =>
+    if @showTrySupport is true && @modalAlreadyExists() && @showTryCount <= @showTryMax
+      @showDelayed()
+      return
+
+    @initalFormParamsIgnore = false
+
+    if @buttonSubmit is true
+      @buttonSubmit = 'Submit'
+    if @buttonCancel is true
+      @buttonCancel = 'Cancel & Go Back'
+
+    @update()
+
+    if @container
+      @el.addClass('modal--local')
+    if @veryLarge
+      @el.addClass('modal--veryLarge')
+    if @large
+      @el.addClass('modal--large')
+    if @small
+      @el.addClass('modal--small')
+
+    @el
+      .on(
+        'show.bs.modal':   @localOnShow
+        'shown.bs.modal':  @localOnShown
+        'hide.bs.modal':   @localOnClose
+        'hidden.bs.modal': @localOnClosed
+        'dismiss.bs.modal': @localOnCancel
+      ).modal(
+        keyboard:  @keyboard
+        show:      true
+        backdrop:  @backdrop
+        container: @container
+      )
+
+    if @closeOnAnyClick
+      @el.on('click', =>
+        @close()
+      )
+
+  close: (e) =>
+    if e
+      e.preventDefault()
+    @initalFormParamsIgnore = true
+    @el.modal('hide')
+
+  formParams: =>
+    if @container
+      return @formParam(@container.find('.modal form'))
+    return @formParam(@$('.modal form'))
+
+  showAlert: (message, suffix = 'danger') ->
+    alert = $('<div>')
+      .addClass("alert alert--#{suffix}")
+      .text(message)
+
+    @$('.modal-alerts-container').html(alert)
+
+  clearAlerts: ->
+    @$('.modal-alerts-container').empty()
+
+  localOnShow: (e) =>
+    @onShow(e)
+
+  onShow: (e) ->
+    # do nothing
+
+  localOnShown: (e) =>
+    @onShown(e)
+
+  onShown: (e) =>
+    if @autoFocusOnFirstInput
+
+      # select generated form
+      form = @$('.form-group').first()
+
+      # if not exists, use whole @el
+      if !form.get(0)
+        form = @el
+
+      # focus first input, select or textarea
+      form.find('input:not([disabled]):not([type="hidden"]):not(".btn"), select:not([disabled]), textarea:not([disabled])').first().focus()
+
+    @initalFormParams = @formParams()
+
+  localOnClose: (e) =>
+    diff = difference(@initalFormParams, @formParams())
+    if @initalFormParamsIgnore is false && !_.isEmpty(diff)
+      if !confirm(App.i18n.translateContent('The form content has been changed. Do you want to close it and lose your changes?'))
+        e.preventDefault()
+        return
+    @onClose(e)
+
+  onClose: ->
+    # do nothing
+
+  localOnClosed: (e) =>
+    @onClosed(e)
+    @el.modal('remove')
+
+  onClosed: (e) ->
+    # do nothing
+
+  localOnCancel: (e) =>
+    @onCancel(e)
+
+  onCancel: (e) ->
+    # do nothing
+
+  cancel: (e) =>
+    @close(e)
+    @onCancel(e)
+
+  onSubmit: (e) ->
+    # do nothing
+
+  submit: (e) =>
+    e.stopPropagation()
+    e.preventDefault()
+    @clearAlerts()
+    @onSubmit(e)
+
+  startLoading: =>
+    @$('.modal-body').addClass('hide')
+    @$('.modal-loader').removeClass('hide')
+
+  stopLoading: =>
+    @$('.modal-body').removeClass('hide')
+    @$('.modal-loader').addClass('hide')

+ 15 - 0
app/assets/javascripts/app/controllers/_application_controller/app_content.coffee

@@ -0,0 +1,15 @@
+class App.ControllerAppContent extends App.Controller
+  constructor: (params) ->
+    if @requiredPermission
+      @permissionCheckRedirect(@requiredPermission)
+
+    # hide tasks
+    App.TaskManager.hideAll()
+
+    params.el = params.appEl.find('#content')
+    params.el.removeClass('hide').removeClass('active')
+    if !params.el.get(0)
+      params.appEl.append('<div id="content" class="content flex horizontal"></div>')
+      params.el = $('#content')
+
+    super(params)

+ 331 - 0
app/assets/javascripts/app/controllers/_application_controller/collection.coffee

@@ -0,0 +1,331 @@
+class App.CollectionController extends App.Controller
+  events:
+    'click .js-remove': 'remove'
+    'click .js-item': 'click'
+    'click .js-locationVerify': 'location'
+  observe:
+    field1: true
+    field2: false
+  #currentItems: {}
+    #1:
+    # a: 123
+    # b: 'some string'
+    #2:
+    # a: 123
+    # b: 'some string'
+  #renderList: {}
+    #1: ..dom..ref..
+    #2: ..dom..ref..
+  template: '_need_to_be_defined_'
+  uniqKey: 'id'
+  model: '_need_to_be_defined_'
+  sortBy: 'name'
+  order: 'ASC',
+  insertPosition: 'after'
+  globalRerender: true
+
+  constructor: ->
+    @events = @constructor.events unless @events
+    @observe = @constructor.observe unless @observe
+    @currentItems = {}
+    @renderList = {}
+    @queue = []
+    @queueRunning = false
+    @lastOrder = []
+
+    super
+
+    @queue.push ['renderAll']
+    @uIRunner()
+
+    # bind to changes
+    if @model
+      @subscribeId = App[@model].subscribe(@collectionSync)
+
+    # render on generic ui call
+    if @globalRerender
+      @controllerBind('ui:rerender', =>
+        @queue.push ['renderAll']
+        @uIRunner()
+      )
+
+    # render on login
+    @controllerBind('auth:login', =>
+      @queue.push ['renderAll']
+      @uIRunner()
+    )
+
+    # reset current tasks on logout
+    @controllerBind('auth:logout', =>
+      @queue.push ['renderAll']
+      @uIRunner()
+    )
+
+    @log 'debug', 'Init @uniqKey', @uniqKey
+    @log 'debug', 'Init @observe', @observe
+    @log 'debug', 'Init @model', @model
+
+  release: =>
+    if @subscribeId
+      App[@model].unsubscribe(@subscribeId)
+
+  uIRunner: =>
+    return if !@queue[0]
+    return if @queueRunning
+    @queueRunning = true
+    loop
+      param = @queue.shift()
+      if param[0] is 'domChange'
+        @domChange(param[1])
+      else if param[0] is 'domRemove'
+        @domRemove(param[1])
+      else if param[0] is 'change'
+        @collectionSync(param[1])
+      else if param[0] is 'destroy'
+        @collectionSync(param[1], 'destroy')
+      else if param[0] is 'renderAll'
+        @renderAll()
+      else
+        @log 'error', "Unknown type #{param[0]}", param[1]
+      if !@queue[0]
+        @onRenderEnd()
+        @queueRunning = false
+        break
+
+  collectionOrderGet: =>
+    newOrder = []
+    all = @itemsAll()
+    for item in all
+      newOrder.push item[@uniqKey]
+    newOrder
+
+  collectionOrderSet: (newOrder = false) =>
+    if !newOrder
+      newOrder = @collectionOrderGet()
+    @lastOrder = newOrder
+
+  collectionSync: (items, type) =>
+
+    # remove items
+    if type is 'destroy'
+      ids = []
+      for item in items
+        ids.push item[@uniqKey]
+      @queue.push ['domRemove', ids]
+      @uIRunner()
+      return
+
+    # inital render
+    if _.isEmpty(@renderList)
+      @queue.push ['renderAll']
+      @uIRunner()
+      return
+
+    # check if item order is the same
+    newOrder = @collectionOrderGet()
+    removedIds = _.difference(@lastOrder, newOrder)
+    addedIds = _.difference(newOrder, @lastOrder)
+
+    @log 'debug', 'collectionSync removedIds', removedIds
+    @log 'debug', 'collectionSync addedIds', addedIds
+    @log 'debug', 'collectionSync @lastOrder', @lastOrder
+    @log 'debug', 'collectionSync newOrder', newOrder
+
+    # add items
+    alreadyRemoved = false
+    if !_.isEmpty(addedIds)
+      lastOrderNew = []
+      for id in @lastOrder
+        if !_.contains(removedIds, id)
+          lastOrderNew.push id
+
+      # try to find positions of new items
+      @log 'debug', 'collectionSync lastOrderNew', lastOrderNew
+      applyOrder = App.Utils.diffPositionAdd(lastOrderNew, newOrder)
+      @log 'debug', 'collectionSync applyOrder', applyOrder
+      if !applyOrder
+        @queue.push ['renderAll']
+        @uIRunner()
+        return
+
+      if !_.isEmpty(removedIds)
+        alreadyRemoved = true
+        @queue.push ['domRemove', removedIds]
+        @uIRunner()
+
+      newItems = []
+      for apply in applyOrder
+        item = @itemGet(apply.id)
+        item.meta_position = apply.position
+        newItems.push item
+      @queue.push ['domChange', newItems]
+      @uIRunner()
+
+    # remove items
+    if !alreadyRemoved && !_.isEmpty(removedIds)
+      @queue.push ['domRemove', removedIds]
+      @uIRunner()
+
+    # update items
+    newItems = []
+    for item in items
+      if !_.contains(removedIds, item.id) && !_.contains(addedIds, item.id)
+        newItems.push item
+    return if _.isEmpty(newItems)
+    @queue.push ['domChange', newItems]
+    @uIRunner()
+    #return
+
+    # rerender all items
+    #@queue.push ['renderAll']
+    #@uIRunner()
+
+  domRemove: (ids) =>
+    @log 'debug', 'domRemove', ids
+    for id in ids
+      @itemAttributesDelete(id)
+      if @renderList[id]
+        @renderList[id].remove()
+        delete @renderList[id]
+      @onRemoved(id)
+    @collectionOrderSet()
+
+  domChange: (items) =>
+    @log 'debug', 'domChange items', items
+    @log 'debug', 'domChange @currentItems', @currentItems
+    changedItems = []
+    for item in items
+      @log 'debug', 'domChange|item', item
+      attributes = @itemAttributes(item)
+      currentItem = @itemAttributesGet(item[@uniqKey])
+      if !currentItem
+        @log 'debug', 'domChange|add', item
+        changedItems.push item
+        @itemAttributesSet(item[@uniqKey], attributes)
+      else
+        @log 'debug', 'domChange|change', item
+        @log 'debug', 'domChange|change|observe attributes', @observe
+        @log 'debug', 'domChange|change|current', currentItem
+        @log 'debug', 'domChange|change|new', attributes
+        for field of @observe
+          @log 'debug', 'domChange|change|compare', field, currentItem[field], attributes[field]
+          diff = !_.isEqual(currentItem[field], attributes[field])
+          @log 'debug', 'domChange|diff', diff
+          if diff
+            changedItems.push item
+            @itemAttributesSet(item[@uniqKey], attributes)
+            break
+    return if _.isEmpty(changedItems)
+    @renderParts(changedItems)
+
+  renderAll: =>
+    items = @itemsAll()
+    @log 'debug', 'renderAll', items
+    localeEls = []
+    for item in items
+      attributes = @itemAttributes(item)
+      @itemAttributesSet(item[@uniqKey], attributes)
+      localeEls.push @renderItem(item, false)
+    @html localeEls
+    @collectionOrderSet()
+    @onRenderEnd()
+
+  itemDestroy: (id) =>
+    App[@model].destroy(id)
+
+  itemsAll: =>
+    App[@model].search(sortBy: @sortBy, order: @order)
+
+  itemAttributesDiff: (item) =>
+    attributes = @itemAttributes(item)
+    currentItem = @itemAttributesGet(item[@uniqKey])
+    for field of @observe
+      @log 'debug', 'itemAttributesDiff|compare', field, currentItem[field], attributes[field]
+      diff = !_.isEqual(currentItem[field], attributes[field])
+      if diff
+        @log 'debug', 'itemAttributesDiff|diff', diff
+        return true
+    false
+
+  itemAttributesDelete: (id) =>
+    delete @currentItems[id]
+
+  itemAttributesGet: (id) =>
+    @currentItems[id]
+
+  itemAttributesSet: (id, attributes) =>
+    @currentItems[id] = attributes
+
+  itemAttributes: (item) =>
+    attributes = {}
+    for field of @observe
+      attributes[field] = item[field]
+    attributes
+
+  itemGet: (id) =>
+    App[@model].find(id)
+
+  renderParts: (items) =>
+    @log 'debug', 'renderParts', items
+    for item in items
+      if !@renderList[item[@uniqKey]]
+        @renderItem(item)
+      else
+        @renderItem(item, @renderList[item[@uniqKey]])
+    @collectionOrderSet()
+
+  renderItem: (item, el) =>
+    if @prepareForObjectListItemSupport
+      item = @prepareForObjectListItem(item)
+    @log 'debug', 'renderItem', item, @template, el, @renderList[item[@uniqKey]]
+    html =  $(App.view(@template)(
+      item: item
+    ))
+    if @onRenderItemEnd
+      @onRenderItemEnd(item, html)
+    itemCount = Object.keys(@renderList).length
+    @renderList[item[@uniqKey]] = html
+    if el is false
+      return html
+    else if !el
+      position = item.meta_position
+      if itemCount > position
+        position += 1
+      element = @el.find(".js-item:nth-child(#{position})")
+      if !element.get(0)
+        @el.append(html)
+        return
+      if @insertPosition is 'before'
+        element.before(html)
+      else
+        element.after(html)
+    else
+      el.replaceWith(html)
+
+  onRenderEnd: ->
+    # nothing
+
+  location: (e) =>
+    @locationVerify(e)
+
+  click: (e) =>
+    row = $(e.target).closest('.js-item')
+    id = row.data('id')
+    @onClick(id, e)
+
+  onClick: (id, e) ->
+    # nothing
+
+  remove: (e) =>
+    e.preventDefault()
+    e.stopPropagation()
+    row = $(e.target).closest('.js-item')
+    id = row.data('id')
+    @onRemove(id,e)
+    @itemDestroy(id)
+
+  onRemove: (id, e) ->
+    # nothing
+
+  onRemoved: (id) ->
+    # nothing

+ 19 - 0
app/assets/javascripts/app/controllers/_application_controller/drox.coffee

@@ -0,0 +1,19 @@
+class App.ControllerDrox extends App.Controller
+  constructor: (params) ->
+    super
+
+    if params.data && ( params.data.text || params.data.html )
+      @inline(params.data)
+
+  inline: (data) ->
+    @html App.view('generic/drox')(data)
+    if data.text
+      @$('.drox-body').text(data.text)
+    if data.html
+      @$('.drox-body').html(data.html)
+
+  template: (data) ->
+    drox = $( App.view('generic/drox')(data) )
+    content = App.view(data.file)(data.params)
+    drox.find('.drox-body').append(content)
+    drox

+ 0 - 0
app/assets/javascripts/app/controllers/_application_controller_form.coffee → app/assets/javascripts/app/controllers/_application_controller/form.coffee


+ 20 - 0
app/assets/javascripts/app/controllers/_application_controller/full_page.coffee

@@ -0,0 +1,20 @@
+class App.ControllerFullPage extends App.Controller
+  constructor: (params) ->
+    if @requiredPermission
+      @permissionCheckRedirect(@requiredPermission)
+    super
+
+  replaceWith: (localElement) =>
+    @appEl.find('>').not(".#{@className}").remove() if @className
+    @appEl.find('>').filter(".#{@className}").remove() if @forceRender
+    @el = $(localElement)
+    container = @appEl.find('>').filter(".#{@className}")
+    if !container.get(0)
+      @el.addClass(@className)
+      @appEl.append(@el)
+      @delegateEvents(@events)
+      @refreshElements()
+      @el.on('remove', @releaseController)
+      @el.on('remove', @release)
+    else
+      container.html(@el.children())

+ 11 - 0
app/assets/javascripts/app/controllers/_application_controller/generic_description.coffee

@@ -0,0 +1,11 @@
+class App.ControllerGenericDescription extends App.ControllerModal
+  buttonClose: true
+  buttonCancel: false
+  buttonSubmit: 'Close'
+  head: 'Description'
+
+  content: =>
+    marked(App.i18n.translateContent(@description))
+
+  onSubmit: =>
+    @close()

+ 37 - 0
app/assets/javascripts/app/controllers/_application_controller/generic_destroy_confirm.coffee

@@ -0,0 +1,37 @@
+class App.ControllerGenericDestroyConfirm extends App.ControllerModal
+  buttonClose: true
+  buttonCancel: true
+  buttonSubmit: 'delete'
+  buttonClass: 'btn--danger'
+  head: 'Confirm'
+  small: true
+
+  content: ->
+    App.i18n.translateContent('Sure to delete this object?')
+
+  onSubmit: =>
+    options = @options || {}
+    options.done = =>
+      @close()
+      if @callback
+        @callback()
+    options.fail = =>
+      @log 'errors'
+      @close()
+    @item.destroy(options)
+
+class App.ControllerConfirm extends App.ControllerModal
+  buttonClose: true
+  buttonCancel: true
+  buttonSubmit: 'yes'
+  buttonClass: 'btn--danger'
+  head: 'Confirm'
+  small: true
+
+  content: ->
+    App.i18n.translateContent(@message)
+
+  onSubmit: =>
+    @close()
+    if @callback
+      @callback()

+ 53 - 0
app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee

@@ -0,0 +1,53 @@
+class App.ControllerGenericEdit extends App.ControllerModal
+  buttonClose: true
+  buttonCancel: true
+  buttonSubmit: true
+  headPrefix: 'Edit'
+
+  content: =>
+    @item = App[ @genericObject ].find( @id )
+    @head = @pageData.head || @pageData.object
+
+    @controller = new App.ControllerForm(
+      model:     App[ @genericObject ]
+      params:    @item
+      screen:    @screen || 'edit'
+      autofocus: true
+      handlers:  @handlers
+    )
+    @controller.form
+
+  onSubmit: (e) ->
+    params = @formParam(e.target)
+    @item.load(params)
+
+    # validate form using HTML5 validity check
+    element = $(e.target).closest('form').get(0)
+    if element && element.reportValidity && !element.reportValidity()
+      return false
+
+    # validate
+    errors = @item.validate()
+    if errors
+      @log 'error', errors
+      @formValidate( form: e.target, errors: errors )
+      return false
+
+    # disable form
+    @formDisable(e)
+
+    # save object
+    ui = @
+    @item.save(
+      done: ->
+        if ui.callback
+          item = App[ ui.genericObject ].fullLocal(@id)
+          ui.callback(item)
+        ui.close()
+
+      fail: (settings, details) ->
+        App[ ui.genericObject ].fetch(id: @id)
+        ui.log 'errors'
+        ui.formEnable(e)
+        ui.controller.showAlert(details.error_human || details.error || 'Unable to update object!')
+    )

Some files were not shown because too many files changed in this diff