Skip to content

Instantly share code, notes, and snippets.

@kerbyfc
Last active November 30, 2015 17:19
Show Gist options
  • Save kerbyfc/8fd9b8242ed0151c42a3 to your computer and use it in GitHub Desktop.
Save kerbyfc/8fd9b8242ed0151c42a3 to your computer and use it in GitHub Desktop.
Backbone TreeCollection, FancyTree super class and it's inheritor ReportsTree ItemView class
"use strict"
mousetrap = require "mousetrap"
module.exports = class FancyTreeBehavior extends Marionette.Behavior
###*
* Default options, that should be merged with
* constructor options argument, and then mixed to
* fancytree view
* @type {Object}
###
defaults: {}
###*
* Fancytree plugin dependencies dnd/filter/etc...
* @type {Array}
###
extensions: []
###
* Methods to be defined in view
###
@methods = {}
hotkeys: {}
###*
* Merge passed and default options, create handlers
* @param {FancyTree} view
* @param {Object} options = {}
###
constructor: (options = {}, @view) ->
@options = _.merge {}, _.result(@, 'defaults'), options
# register hotkeys
for hotkeys, method of _.result(@, 'hotkeys')
mousetrap.bind hotkeys, @[method].bind @
@on "destroy", @_unbindKeys
# register fancytree extensions
@view.options.extensions = _.union (@view.options.extensions or []), @extensions
# define view methods
for method, reqres of @methods
reqres = _.kebabCase(method).replace /\-/g, ":"
unless @[method]
throw new Error "#{@constructor.name}.#{method} implementation missed"
@view._defineMethod _.camelCase(method), reqres, @[method], @
super
_unbindKeys: =>
for hotkeys, method of _.result(@, 'hotkeys')
mousetrap.unbind hotkeys
"use strict"
FancyTreeBehavior = require "views/controls/fancytree/behavior.coffee"
module.exports = class ActivityManager extends FancyTreeBehavior
###*
* Css class selectors that affects on activity disabling
* @type {Array}
###
defaults:
###*
* Css class selectors that affects on activity disabling
* @override
* @type {Array}
###
resetTriggers: [ ".fancytree-container" ]
methods: {
"setActiveNode"
"getActiveNode"
"getActiveFolder"
"getActiveItem"
"resetNodesActivity"
}
onShow: ->
for trigger in @options.resetTriggers
el = @view.$ trigger
# it can be parent container, so we should find it
# only in current tree branch to avoid extra bugs
el = el.length and el or @view.$el.closest trigger
el.on "click", @_resetNodesActivity
###*
* Unregister event handlers
###
beforeDestroy: ->
for trigger in @options.resetTriggers
el = @$ trigger
el = el.length and el or @$el.closest trigger
el.off "click"
###########################################################################
# PRIVATE
###*
* Handle background clicks to reset active node
* @param {Event} e - event
###
_resetNodesActivity: (e) =>
if @_isActivityResetTrigger e.target
@resetNodesActivity()
###*
* Check if node is matched to selector
* @param {Event} e
* @return {Boolean}
###
_isActivityResetTrigger: (node) =>
el = $ node
_.any @options.resetTriggers, (trigger) ->
el.is trigger
###########################################################################
# INTERFACE
###*
* Deactive current node
* @param {FancytreeNode} root = @tree.rootNode
###
resetNodesActivity: (root = @view.tree?.rootNode) ->
if root
root.visit (node) ->
node.setFocus false
node.setActive false
@triggerMethod "reset:nodes:activity"
###*
* Return tree active node
* @return {FancytreeNode|Null} tree node or null
###
getActiveNode: ->
@view.tree?.getActiveNode() or null
###*
* Return active report
* @return {FancytreeNode|Null} node or null
###
getActiveItem: ->
if node = @getActiveNode()
unless node.parent
return node
null
###*
* Return active report
* @return {FancytreeNode|Null} node or null
###
getActiveFolder: ->
if node = @getActiveNode()
return switch
when @view.isFolder node
node
else
if not @view.isRootNode node.parent
node.parent
else
null
null
###*
* Activate/deactivate node
* @param {String} key - node key
* @param {Boolean} flag = true
* @param {Object} opts = {} - for example noEvents:true
###
setActiveNode: (key, flag = true, opts = {}) ->
if node = @view.getNode key
node.setActive flag, opts
return node
null
"use strict"
helpers = require "common/helpers.coffee"
FancyTreeBehavior = require "../behavior.coffee"
module.exports = class FancyTreeSearchBehavior extends FancyTreeBehavior
methods: {
"search"
"resetSearchQuery"
"getSearchQuery"
}
###*
* Default values for fancytree dnd extension
* @type {Object}
###
defaults:
quicksearch : true
container : false
input : "[data-search] > input"
template : "controls/fancytree/search"
value : ""
placeholder : ""
autoApply : true
autoExpand : true
mode : "hide"
hotkeys:
focus: "alt+e"
###*
* Options to mixin to fanctytree options
* @type {Array}
###
exportOptions: [
"autoApply"
"autoExpand"
"mode"
"quicksearch"
]
hotkeys: ->
helpers.toObject @options.hotkeys.focus, "focus"
###*
* Fancytree plugin dependencies
* @type {Array}
###
extensions: ['filter']
initialize: ->
unless @options.container
throw new Error "Search extension needs `container` option"
# mixin options to fancytree
_.merge @view.options,
filter: _.pick @options, @exportOptions...
onShow: =>
@container = @view.$ @options.container
@container.html Marionette.Renderer.render @options.template, @options
@input = @container.find @options.input
@input.on "keyup", @_search
###########################################################################
# PRIVATE
###*
* Clear or change filter
###
_search: (e) =>
if e.which is $.ui.keyCode.ESCAPE or
not $.trim @input.val()
@resetSearchQuery()
else
@search @input.val()
###########################################################################
# INTERFACE
focus: ->
@input.focus()
###*
* Filter nodes with search query
* @param {String} query
###
search: (query) =>
if @query isnt query and @view.tree?
@query = query
@view.tree.filterNodes @query, @options
@input.val query
###*
* Clear search
###
resetSearchQuery: ->
@query = ""
@input.val @query
@view.tree.clearFilter()
###*
* Get or set value
* @param {String} value - value to set
###
getSearchQuery: ->
@input.val value
"use strict"
require "fancytree"
behaviorClasses =
activity : require "./behaviors/activity.coffee"
dnd : require "./behaviors/dnd.coffee"
expander : require "./behaviors/expander.coffee"
node : require "./behaviors/node.coffee"
search : require "./behaviors/search.coffee"
selection : require "./behaviors/selection.coffee"
visitor : require "./behaviors/visitor.coffee"
stateful : require "./behaviors/stateful.coffee"
###*
* Universally light-weight tree view, that uses fancytree.
*
* Next methods might be implemented:
* - getSource
*
* Public methods are wrapped to be requirable by App.reqres
* - getActive[Item/Folder/Node]
* - getSelected[Item/Folder/Node]
* - others...
*
* @note Next constructor arguments are required: <String/jQuery> container
* @note Next properties must be implemented: <String> scope
* @note See methods might me defined to handle tree events se _buildTree
*
* @example extending
* class ReportsTreeView extends FancyTree
*
* # to be able to handle events by App.vent "reports:tree:<event>"
* scope: "reports"
*
* onNodeActivate: (node, data) ->
* console.log data.model.id, data.attrs.DISPLAY_NAME
*
* @example instantiating
* tree = new ReportsTreeView
* container: ".tree-view__container"
*
* @example add handlers
* tree.onFolderSelect = (node, data) -> ...
*
* @example listen events
* App.vent.on "reports:tree:item:select", (node, data) -> ...
*
* @see views/controls/fancytree/*.coffee
* @example Create extension
*
* # my_tree.coffee
* couter = require(...counter.coffee)
*
* class MyTree extends FancyTree
*
* extensions: _.extend FancyTree::extensions,
* counter: counter
*
* ...
*
* # counter.coffee
*
* class CounterFancyTreeExtention
*
* # you can pass options to extension via options.counter
* # and merge them with defaults
* defaults:
* counter: {}
*
* contructor: (view, options = {}) ->
* ...
*
* # use fancytree virtual methods to
* # handle fancytree events
* onNodeClick : -> ...
* onItemSelect : -> ...
*
* @note LayoutView is used as it's more extensible
*
###
class FancyTree extends Marionette.LayoutView
###*
* Default fancytree settings
* @type {Object}
###
defaults:
checkbox : true
icons : false
selectMode : 3
paths:
# paths to model (in node data)
# to interract with
# @note required for some behaviors
model: "attrs.model"
methods: {
"rebuild"
}
behaviorClasses: behaviorClasses
# default behaviors
behaviors: ->
node : {}
visitor : {}
activity : {}
expander : {}
selection : {}
###*
* Properties to be checked while instantiation
* @type {Array}
###
requiredProps: [
"scope"
"container"
]
###*
* Validate self, register handlers for application event bus,
* instantiate extensions
* @throws {Error} If required props are missing
* @param {Object} options = {}
###
constructor: (options = {}) ->
# extend & override default fancytree options
options = _.extend {}, (@options or {}), options
@options = _.defaults {}, options, _.result @, 'defaults'
# setup required props
for prop in @requiredProps
if options[prop]
@[prop] = options[prop]
unless @[prop]
throw new Error "FancyTree: `#{prop}` must be specified
or passed as option"
# define view methods
for method, reqres in @methods
reqres = _.kebabCase(method).replace /\-/g, ":"
view._defineMethod _.camelCase(method), reqres, @[method]
# make behaviors hash
@behaviors = _.reduce _.result(@, 'behaviors'), (acc, options, behavior) =>
acc[behavior] =
if behaviorClass = @behaviorClasses[behavior]
_.extend {}, options, behaviorClass: behaviorClass
else
options
acc
, {}
super
###*
* Build tree on show
###
onShow: ->
@_rebuild()
###########################################################################
# PRIVATE
###*
* Create tree event handler
* @param {String} eventSign - "on/before:event"
###
_createEventHandler: (eventSign) =>
(e, data, node = data.node) =>
if e.type.match "click"
e.stopImmediatePropagation()
type = @isFolder(node) and "folder" or "item"
[ moment, event ] = eventSign.split ":"
@_triggerInteractionEvent moment, event, node, data, type, e
###*
* Trigger event with App.vent bus, call proper handler
* @example do things before and after item node selection
* tree.on "before:node:select"
* tree.on "before:item:select"
* tree.on "node:select"
* tree.on "node:item:select"
* @param {String} moment - "on"/"before"
* @param {Event} event
* @param {Array} args...
###
_triggerInteractionEvent: (moment, event, args...) ->
type = args[2]
prefix = moment is "before" and "before:" or ""
# trigger events
@triggerMethod "#{prefix}node:#{event.toLowerCase()}", args...
@triggerMethod "#{prefix}#{type}:#{event.toLowerCase()}", args...
triggerMethod: (args...) ->
super
App.vent.trigger "#{@scope}:tree:#{_.first args}"
###*
* Instantiate fancytree
###
_buildTree: ->
container = $ @container
return unless container.length
handlers = _.reduce [
"on:select" # on[Node/Item/Folder]Select method
"on:activate"
"on:deactivate"
"on:focus"
"on:blur"
"on:expand"
"on:collapse"
"on:click"
"on:dblclick"
"on:lazyLoad"
], (acc, event) =>
acc[_.last event.split ":"] = @_createEventHandler event
acc
, {}
# if handler was not specified by view,
# but registered by extension,
# then extend handlers with them
for extension in _.values @extensions
if _handlers = extension.registerHandlers?()
for handler, event of _handlers
unless handlers[handler]
handlers[handler] = @_createEventHandler event
options = _.extend @options, handlers,
source: =>
source = @getSource()
@triggerMethod "getSource", source
source
# # special handler naming convention
removeNode : @_createEventHandler "on:remove"
renderNode : @_createEventHandler "on:render"
# # before[Node/Item/Folder]Select method
beforeSelect : @_createEventHandler "before:select"
beforeExpand : @_createEventHandler "before:expand"
beforeActivate : @_createEventHandler "before:activate"
@log ":options", options
container.fancytree options
if @options.autoSort
# this class disables arrows while drag'n'drop
container.children(":first").addClass "_autoSort"
@tree = container.fancytree 'getTree'
@triggerMethod "build", @tree
_defineMethod: (methodName, reqres, implementation, context = @) ->
# If method exists in view, it shouldn't be overriden by extension,
# instead, in this case methods, defined by view class
# overrites method that was defined by extension)
@[methodName] ?= ->
implementation.apply context, arguments
App.reqres.setHandler "#{@scope}:tree:#{reqres}", @[methodName]
###*
* Rebuild tree, reactivate currently active node
###
_rebuild: ->
unless @tree
@_buildTree()
else
active = @getActiveNode()
@tree.reload()
# try to activate node after rebuild
# if there are no active node right after reload
if active and not @getActiveNode()
if node = @getNode active.key
node.setActive true, noEvents: true
@trigger "rebuild", @tree
###########################################################################
# PUBLIC
###*
* Check if node is a folder node
* @param {FancytreeNode} node
* @return {Boolean}
###
isFolder: (node) ->
node.folder
rebuild: ->
@_rebuild()
getSource: -> []
module.exports = FancyTree
"use strict"
FancyTree = require "views/controls/fancytree/view.coffee"
module.exports = class ReportsTree extends FancyTree
template : "reports/tree"
className : "sidebar__content"
scope : "reports"
options:
icons : true
checkbox : false
expanded : 2
behaviors: ->
_.extend super,
dnd: {}
stateful: {}
search:
placeholder : App.t "reports.search_placeholder"
container : "[data-widget='fancyTreeSearch']"
activity:
resetTriggers: [
".fancytree-container"
".sidebar__header"
".sidebar__indent"
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment