'use strict'; define [ './states/index', './presenters/default', './presenters/default', '/assets/jquery/inview', 'stampit/stampit'], (templates, presenter, inview, stampit) -> observable = require('observable').mixin advisable = require('advisable').mixin # TODO define componentjs required packages, as requirejs packages stampit ||= require 'stampit/stampit' scopable = (widget) -> deferred = widget.sandbox.data.deferred() # TODO add widget plug-in as an extension for a widget require ['widgets/viewer/plugins/scopable'], (scopable) -> deferred.resolveWith scopable, [scopable widget] deferred paginable = stampit flip_to: (page) -> @widget.scope.page (page - 1) @flip() flip: -> {scope} = @widget {page_number, total_pages} = scope return unless total_pages? scope.page ++page_number if page_number <= total_pages @widget.scope_to scope else @widget.sandbox.emit "#{@widget.name}.#{@widget.identifier}.last_page" , {} , -> {sandbox, scope} = @widget {page_number} = scope scope.total_pages ?= Infinity unless scope.page? page_number throw new TypeError "Pagination could not be initialized required method scope#page not found!" # TODO scope.subscribe 'page_number', total_pages sandbox.on "#{@widget.name}.#{@widget.identifier}.flip" , @flip , @ sandbox.on "#{@widget.name}.#{@widget.identifier}.flip_to" , @flip_to , @ stampit.mixIn @, @widget.options.pagination scrollable = stampit bottoned: -> scrollBottom = @scroll_container.scrollTop() + @scroll_container.height() scrollableBottom = @widget.$el.height() + @widget.$el.offset().top scrollBottom + @buffer > scrollableBottom scrolled: -> @widget.sandbox.emit "#{@widget.name}.#{@widget.identifier}.flip" if @bottoned() , buffer: 400 , -> @scroll_container = $ window @scroll_container.scroll _.throttle (params...) => @scrolled params... , 500 # Trigger more items loading if page starts in bottom state # TODO Account for autofetchable viewer @widget.sandbox.on "viewer.#{@widget.identifier}.populated", @scrolled, @ stampit.mixIn @, @widget.options.scroll boo = cache: {} initialize: (container) -> container.children('.item') .each(@identify) .on('inview', @viewed) identify: (index, element) -> element = $ element identity = _.uniqueId() element.data 'boo', identity boo.cache[identity] = element shame: (element) -> element = $ element identity = element.data 'boo' child = element.children() # We must store the current element state, we do so by storing a # custom tailored object in our object cache ghost = child: child shamed: true boo.cache[identity] = ghost element.css width: element.width() height: element.height() visibility: 'hidden' # child.detach() pride: (element) -> element = $ element identity = element.data 'boo' ghost = boo.cache[identity] if ghost and ghost.shamed ghost.shamed = false # In order to remove staticaly set width and height we pass # empty strings to css jquery method element.css width: '', height: '', visibility: '' viewed: (event, in_view, horizontal, vertical) -> boo[if in_view then 'pride' else 'shame'] event.target version: '0.2.2' # TODO better separation of concerns # TODO Current remote page that is beign displayed options: resource: 'default' # TODO rename records to resources records: null # Automatically fetch records on initialization autofetch: false # If page attribute is set, viewer will assume that there is a # page method on the scope page: null scroll: null type: 'Base' presenter: presenter select: (item) -> @selected_item?.selected = false @selected_item = item item.selected = true # We extend presentation.selected just to assign all values of the item model # TODO call presenter to do this job @sandbox.util.extend @presentation.selected , item.model.json?() || item.model # TODO change paramters to item, item.model @sandbox.emit "viewer.#{@identifier}.selected", item.model # Called when hover in and out from model hover: (item) -> # TODO call presenter to do this job # @sandbox.util.extend @presentation.hovered , item.model.json?() || item.model @sandbox.emit "viewer.#{@identifier}.hovered", item, item && item.model scope_to: (scope, child_scope) -> # Singuralize in order to accept association scopes, since # association scopes return almost the same kind as of it's # singularized version sent_scope = @inflector.singularize scope.resource.toString() current_scope = @inflector.singularize @scope.resource.toString() if sent_scope != current_scope throw new TypeError "Invalid scope sent to viewer@#{@identifier} sent: '#{sent_scope}', expected: '#{current_scope}'" # For sobsequent usages we must store the scope @scope = scope # TODO better hierachical event distribution for { _widget: widget } in @sandbox._children? widget.scope_to? child_scope @sandbox.emit "viewer.#{@identifier}.scope_changed", @scope # TODO better scope data binding, and updating if @view? and scope.scope?.data @view.update scope_data: observable scope.scope.data @repopulate() repopulate: -> unless @fetching? if @load? @load.stop() @load = null else @fetching?.abort?() # TODO store spinner instance, instead of creating a new one every time unless @load? @load = @sandbox.ui.loader @$el.find '.results .items' # TODO implement status for viewer widget @$el.addClass 'idle' @$el.removeClass 'loading' {viewer} = @presentation # ✔ Generalize this filtering option # TODO make scope.all method use scope too, and replace @scope.fetch by it options = @options # TODO better options accessing @fetching = @scope.fetch null, (records) => # TODO instantiate records before calling this callback records = _.map records, @resource, @resource unless records[0]?.resource or records[0]?.itemable # TODO implement Array.concat ou Array.merge in observer, and # use it here instead of pushing each record viewer.items = records # Start widgets created by bindings @syncronize_children() @fetching.done (records) => if viewer.items.length # boo.initialize @$el.find '.results .items' @$el.addClass 'filled' @$el.removeClass 'empty' else # TODO implement state support for viewer widget @$el.addClass 'empty' @$el.removeClass 'filled' @sandbox.emit "viewer.#{@identifier}.populated", records, @ @fetching.always => # TODO implement status for viewer widget @$el.addClass 'idle' @$el.removeClass 'loading' if @load? @load.stop() @load = null populate: (handlers) -> sandbox = @sandbox @load = @sandbox.ui.loader @$results # TODO implement status for viewer widget @$el.removeClass 'idle' @$el.addClass 'loading' # TODO replace with strategy pattern, please! if @options.records?.length deferred = jQuery.Deferred() deferred.resolveWith @scope, [@options.records] else if @options.autofetch deferred = @scope.all() else deferred = jQuery.Deferred() deferred.resolveWith @scope, [[]] # Initialize dependencies # TODO replace with strategy pattern, please! deferred.done (records) => @load.stop() @presentation = @presenter records, @scope @$el.html templates[@options.resource] if records.length # boo.initialize @$el.find '.results .items' @$el.addClass 'filled' else @$el.addClass 'empty' # TODO move binders to application @inherit_parent_presentation() @bind @presentation, @presenter.presentation # Start widgets that may have been created by bindings @sandbox.emit 'aura.sandbox.start', @sandbox @syncronize_children() @handles 'click', 'back', '.back' @sandbox.emit "viewer.#{@identifier}.populated", records, @ deferred.fail => # TODO better error message and viewer status @html 'Failed to fetch data from server.' plugins: (options) -> deferreds = [] deferreds.push paginable widget: @ if options.page deferreds.push scrollable widget: @ if options.scroll deferreds.push scopable @ if options.scope or options.scopable @sandbox.data.when deferreds... # TODO move this method to an extension syncronize_children: -> @sandbox._children ||= [] @sandbox._widget ||= @ # Add possible new childs @constructor.startAll(@$el).done (widgets...) => for widget in widgets widget.sandbox._widget = widget widget.sandbox._parent = @sandbox @sandbox._children = @sandbox._children.concat widgets for widget in widgets # TODO emit this event only when all siblings have initialized @sandbox.emit "#{widget.name}.#{widget.identifier}.siblings_initialized", @sandbox._children true # TODO better internal aura widget selection # Prevent other child to be instantiated @$el.find('[data-aura-widget]').each (i, element) -> current = element.getAttribute 'data-aura-widget' element.removeAttribute 'data-aura-widget' element.setAttribute 'aura-widget', current # TODO move this method to an extension inherit_parent_presentation: -> return unless view = @sandbox?._parent?._view advisable view unless view.after? # TODO move this method to sandbox isDescendant = (parent, child) -> node = child.parentNode while (node != null) return true if (node == parent) node = node.parentNode false inherited = [] # Copy default models # TODO think if its a good idea to notify about model name conflicts for name, model of view.models when not @presentation[name] # By default do not override child models with parent models @presentation[name] = model inherited.push name # TODO store bindings instead of searching every time for binding in view.bindings when binding.iterated for subview in binding.iterated if isDescendant subview.els[0], @$el.get(0) for name, model of subview.models when not @presentation[name] advisable subview @presentation[name] = model inherited.push name break # Schedule update of copied models view.after 'update', (models) => @update_inherited_models view, models, inherited subview.after 'update', (models) => @update_inherited_models view, models, inherited true update_inherited_models: (parent, models, inherited) -> # Only update inherited models models = @sandbox.util._.pick models, inherited isDescendant = (parent, child) -> node = child.parentNode while (node != null) return true if (node == parent) node = node.parentNode false # Copy default models # TODO think if its a good idea to notify about model name conflicts for name, model of models @presentation[name] = model @view.update models # TODO store bindings instead of searching every time for binding in parent.bindings when binding.iterated for subview in binding.iterated if isDescendant subview.els[0], @$el.get(0) models = @sandbox.util._.pick subview.models, inherited @view.update models @presentation[name] = model for model in models break true initialize: (options) -> # TODO import core extensions in another place @resource = @sandbox.resource options.resource @scope = model = @resource {sandbox: {util: {@inflector}}} = @ @sandbox.on "viewer.#{@identifier}.scope", @scope_to, @ # Iniitalize plugins loading = @plugins options @$el.addClass "viewer widget #{@inflector.cssify @identifier} idle clearfix" loading.done => @require_custom options require_custom: (options) -> # Fetch custom templates # TODO better custom templates structure and custom presenter # TODO better segregation of concerns on this code # TODO handle case where custom presenter does not exist! require [ "text!./widgets/viewer/templates/default/#{options.resource}.html" "./widgets/viewer/presenters/#{options.resource}" ], (custom_default_template, custom_presenter) => # TODO Better way to preserve widgets handlers # TODO Move each handler to independent features handlers = item: hover: (event, models) => if event.type == 'mouseenter' @hover models.item else if event.type == 'mouseleave' @hover null else throw new TypeError 'viewer.handlers.hover: Event type incompatible with hovering.' clicked: (event, models) => @select models.item presenter.handlers = handlers custom_default_template and templates[options.resource] = custom_default_template @presenter = @sandbox.util.extend custom_presenter, presenter if custom_presenter @$results = @$el.find '.results .items' # Fetch default data @populate handlers true