Added tree selection attribute for object manager.
This commit is contained in:
parent
284dbb3de8
commit
fb8130da18
23 changed files with 819 additions and 91 deletions
|
@ -15,7 +15,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
attribute: attribute
|
||||
params: params
|
||||
))
|
||||
@[localParams.data_type](element, localParams, params)
|
||||
@[localParams.data_type](element, localParams, params, attribute)
|
||||
localItem.find('.js-dataMap').html(element)
|
||||
localItem.find('.js-dataScreens').html(@dataScreens(attribute, localParams, params))
|
||||
|
||||
|
@ -24,6 +24,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
date: 'Date'
|
||||
input: 'Text'
|
||||
select: 'Select'
|
||||
tree_select: 'Tree Select'
|
||||
boolean: 'Boolean'
|
||||
integer: 'Integer'
|
||||
|
||||
|
@ -308,6 +309,69 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
lastSelected = value
|
||||
)
|
||||
|
||||
@buildRow: (element, child, level = 0, parentElement) ->
|
||||
newRow = element.find('.js-template').clone().removeClass('js-template')
|
||||
newRow.find('.js-key').attr('level', level)
|
||||
newRow.find('.js-key').val(child.name)
|
||||
newRow.find('td').first().css('padding-left', "#{(level * 20) + 10}px")
|
||||
if level is 5
|
||||
newRow.find('.js-addChild').addClass('hide')
|
||||
|
||||
if parentElement
|
||||
parentElement.after(newRow)
|
||||
return
|
||||
|
||||
element.find('.js-treeTable').append(newRow)
|
||||
if child.children
|
||||
for subChild in child.children
|
||||
@buildRow(element, subChild, level + 1)
|
||||
|
||||
@tree_select: (item, localParams, params, attribute) ->
|
||||
params.data_option ||= {}
|
||||
params.data_option.options ||= []
|
||||
if _.isEmpty(params.data_option.options)
|
||||
@buildRow(item, {})
|
||||
else
|
||||
for child in params.data_option.options
|
||||
@buildRow(item, child)
|
||||
|
||||
item.on('click', '.js-addRow', (e) =>
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
addRow = $(e.currentTarget).closest('tr')
|
||||
level = parseInt(addRow.find('.js-key').attr('level'))
|
||||
@buildRow(item, {}, level, addRow)
|
||||
)
|
||||
|
||||
item.on('click', '.js-addChild', (e) =>
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
addRow = $(e.currentTarget).closest('tr')
|
||||
level = parseInt(addRow.find('.js-key').attr('level')) + 1
|
||||
@buildRow(item, {}, level, addRow)
|
||||
)
|
||||
|
||||
item.on('click', '.js-remove', (e) ->
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.stopPro
|
||||
element = $(e.target).closest('tr')
|
||||
level = parseInt(element.find('.js-key').attr('level'))
|
||||
subElements = 0
|
||||
nextElement = element
|
||||
elementsToDelete = [element]
|
||||
loop
|
||||
nextElement = nextElement.next()
|
||||
break if !nextElement.get(0)
|
||||
nextLevel = parseInt(nextElement.find('.js-key').attr('level'))
|
||||
break if nextLevel <= level
|
||||
subElements += 1
|
||||
elementsToDelete.push nextElement
|
||||
return if subElements isnt 0 && !confirm("Delete #{subElements} sub elements?")
|
||||
for element in elementsToDelete
|
||||
element.remove()
|
||||
)
|
||||
|
||||
@boolean: (item, localParams, params) ->
|
||||
lastSelected = undefined
|
||||
item.on('click', '.js-selected', (e) ->
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.tree_select extends App.UiElement.ApplicationUiElement
|
||||
@optionsSelect: (children, value) ->
|
||||
return if !children
|
||||
for child in children
|
||||
if child.value is value
|
||||
child.selected = true
|
||||
if child.children
|
||||
@optionsSelect(child.children, value)
|
||||
|
||||
@render: (attribute, params) ->
|
||||
|
||||
# set multiple option
|
||||
if attribute.multiple
|
||||
attribute.multiple = 'multiple'
|
||||
else
|
||||
attribute.multiple = ''
|
||||
|
||||
# build options list based on config
|
||||
@getConfigOptionList(attribute, params)
|
||||
|
||||
# build options list based on relation
|
||||
@getRelationOptionList(attribute, params)
|
||||
|
||||
# add null selection if needed
|
||||
@addNullOption(attribute, params)
|
||||
|
||||
# sort attribute.options
|
||||
@sortOptions(attribute, params)
|
||||
|
||||
# finde selected/checked item of list
|
||||
if attribute.options
|
||||
@optionsSelect(attribute.options, attribute.value)
|
||||
|
||||
# disable item of list
|
||||
@disabledOptions(attribute, params)
|
||||
|
||||
# filter attributes
|
||||
@filterOption(attribute, params)
|
||||
|
||||
new App.SearchableSelect(attribute: attribute).element()
|
|
@ -1,4 +1,46 @@
|
|||
# coffeelint: disable=duplicate_key
|
||||
treeParams = (e, params) ->
|
||||
tree = []
|
||||
lastLevel = 0
|
||||
lastLevels = []
|
||||
valueLevels = []
|
||||
|
||||
$(e.target).closest('.modal').find('.js-treeTable .js-key').each( ->
|
||||
$element = $(@)
|
||||
level = parseInt($element.attr('level'))
|
||||
name = $element.val()
|
||||
item =
|
||||
name: name
|
||||
|
||||
if level is 0
|
||||
tree.push item
|
||||
else if lastLevels[level-1]
|
||||
lastLevels[level-1].children ||= []
|
||||
lastLevels[level-1].children.push item
|
||||
else
|
||||
console.log('ERROR', item)
|
||||
if level is 0
|
||||
valueLevels = []
|
||||
else if lastLevel is level
|
||||
valueLevels.pop()
|
||||
else if lastLevel > level
|
||||
down = lastLevel - level
|
||||
for count in [1..down]
|
||||
valueLevels.pop()
|
||||
if lastLevel <= level
|
||||
valueLevels.push name
|
||||
|
||||
item.value = valueLevels.join('::')
|
||||
lastLevels[level] = item
|
||||
lastLevel = level
|
||||
|
||||
)
|
||||
if tree[0]
|
||||
if !params.data_option
|
||||
params.data_option = {}
|
||||
params.data_option.options = tree
|
||||
params
|
||||
|
||||
class Index extends App.ControllerTabs
|
||||
requiredPermission: 'admin.object'
|
||||
constructor: ->
|
||||
|
@ -135,6 +177,7 @@ class New extends App.ControllerGenericNew
|
|||
|
||||
onSubmit: (e) =>
|
||||
params = @formParam(e.target)
|
||||
params = treeParams(e, params)
|
||||
|
||||
# show attributes for create_middle in two column style
|
||||
if params.screens && params.screens.create_middle
|
||||
|
@ -184,6 +227,8 @@ class Edit extends App.ControllerGenericEdit
|
|||
#if attribute.name is 'data_type'
|
||||
# attribute.disabled = true
|
||||
|
||||
console.log('configure_attributes', configure_attributes)
|
||||
|
||||
@controller = new App.ControllerForm(
|
||||
model:
|
||||
configure_attributes: configure_attributes
|
||||
|
@ -195,6 +240,7 @@ class Edit extends App.ControllerGenericEdit
|
|||
|
||||
onSubmit: (e) =>
|
||||
params = @formParam(e.target)
|
||||
params = treeParams(e, params)
|
||||
|
||||
# show attributes for create_middle in two column style
|
||||
if params.screens && params.screens.create_middle
|
||||
|
|
|
@ -390,14 +390,14 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
properties:
|
||||
translateX: 0
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
|
||||
# fade out list
|
||||
@recipientList.velocity
|
||||
properties:
|
||||
translateX: '-100%'
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
complete: => @recipientList.height(@organizationList.height())
|
||||
|
||||
hideOrganizationMembers: (e) =>
|
||||
|
@ -413,7 +413,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
properties:
|
||||
translateX: 0
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
|
||||
# reset list height
|
||||
@recipientList.height('')
|
||||
|
@ -423,7 +423,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
properties:
|
||||
translateX: '100%'
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
complete: => @organizationList.addClass('hide')
|
||||
|
||||
newObject: (e) ->
|
||||
|
|
|
@ -5,15 +5,21 @@ class App.SearchableSelect extends Spine.Controller
|
|||
'blur .js-input': 'onBlur'
|
||||
'focus .js-input': 'onFocus'
|
||||
'click .js-option': 'selectItem'
|
||||
'click .js-enter': 'navigateIn'
|
||||
'click .js-back': 'navigateOut'
|
||||
'mouseenter .js-option': 'highlightItem'
|
||||
'mouseenter .js-enter': 'highlightItem'
|
||||
'mouseenter .js-back': 'highlightItem'
|
||||
'shown.bs.dropdown': 'onDropdownShown'
|
||||
'hidden.bs.dropdown': 'onDropdownHidden'
|
||||
|
||||
elements:
|
||||
'.js-option': 'option_items'
|
||||
'.js-dropdown': 'dropdown'
|
||||
'.js-option, .js-enter': 'optionItems'
|
||||
'.js-input': 'input'
|
||||
'.js-shadow': 'shadowInput'
|
||||
'.js-optionsList': 'optionsList'
|
||||
'.js-optionsSubmenu': 'optionsSubmenu'
|
||||
'.js-autocomplete-invisible': 'invisiblePart'
|
||||
'.js-autocomplete-visible': 'visiblePart'
|
||||
|
||||
|
@ -27,32 +33,95 @@ class App.SearchableSelect extends Spine.Controller
|
|||
@render()
|
||||
|
||||
render: ->
|
||||
firstSelected = _.find @options.attribute.options, (option) -> option.selected
|
||||
firstSelected = _.find @attribute.options, (option) -> option.selected
|
||||
|
||||
if firstSelected
|
||||
@options.attribute.valueName = firstSelected.name
|
||||
@options.attribute.value = firstSelected.value
|
||||
else if @options.attribute.unknown && @options.attribute.value
|
||||
@options.attribute.valueName = @options.attribute.value
|
||||
@attribute.valueName = firstSelected.name
|
||||
@attribute.value = firstSelected.value
|
||||
else if @attribute.unknown && @attribute.value
|
||||
@attribute.valueName = @attribute.value
|
||||
else if @hasSubmenu @attribute.options
|
||||
@attribute.valueName = @getName @attribute.value, @attribute.options
|
||||
|
||||
@options.attribute.renderedOptions = App.view('generic/searchable_select_options')
|
||||
options: @options.attribute.options
|
||||
@html App.view('generic/searchable_select')
|
||||
attribute: @attribute
|
||||
options: @renderAllOptions '', @attribute.options, 0
|
||||
submenus: @renderSubmenus @attribute.options
|
||||
|
||||
@html App.view('generic/searchable_select')( @options.attribute )
|
||||
# initial data
|
||||
@currentMenu = @findMenuContainingValue @attribute.value
|
||||
@level = @getIndex @currentMenu
|
||||
|
||||
@input.on 'keydown', @navigate
|
||||
renderSubmenus: (options) ->
|
||||
html = ''
|
||||
if options
|
||||
for option in options
|
||||
if option.children
|
||||
html += App.view('generic/searchable_select_submenu')
|
||||
options: @renderOptions(option.children)
|
||||
parentValue: option.value
|
||||
title: option.name
|
||||
|
||||
if @hasSubmenu(option.children)
|
||||
html += @renderSubmenus option.children
|
||||
html
|
||||
|
||||
hasSubmenu: (options) ->
|
||||
return false if !options
|
||||
for option in options
|
||||
return true if option.children
|
||||
return false
|
||||
|
||||
getName: (value, options) ->
|
||||
for option in options
|
||||
if option.value is value
|
||||
return option.name
|
||||
if option.children
|
||||
name = @getName value, option.children
|
||||
return name if name isnt undefined
|
||||
undefined
|
||||
|
||||
renderOptions: (options) ->
|
||||
html = ''
|
||||
for option in options
|
||||
html += App.view('generic/searchable_select_option')
|
||||
option: option
|
||||
class: if option.children then 'js-enter' else 'js-option'
|
||||
html
|
||||
|
||||
renderAllOptions: (parentName, options, level) ->
|
||||
html = ''
|
||||
if options
|
||||
for option in options
|
||||
className = if option.children then 'js-enter' else 'js-option'
|
||||
if level && level > 0
|
||||
className += ' is-hidden is-child'
|
||||
|
||||
html += App.view('generic/searchable_select_option')
|
||||
option: option
|
||||
class: className
|
||||
detail: parentName
|
||||
|
||||
if option.children
|
||||
html += @renderAllOptions "#{parentName} — #{option.name}", option.children, level+1
|
||||
html
|
||||
|
||||
onDropdownShown: =>
|
||||
@input.on 'click', @stopPropagation
|
||||
@highlightFirst()
|
||||
$(document).on 'keydown.searchable_select', @navigate
|
||||
if @level > 0
|
||||
@showSubmenu(@currentMenu)
|
||||
@isOpen = true
|
||||
|
||||
onDropdownHidden: =>
|
||||
@input.off 'click', @stopPropagation
|
||||
@option_items.removeClass '.is-active'
|
||||
@unhighlightCurrentItem()
|
||||
$(document).off 'keydown.searchable_select'
|
||||
@isOpen = false
|
||||
|
||||
toggle: =>
|
||||
@currentItem = null
|
||||
@$('[data-toggle="dropdown"]').dropdown('toggle')
|
||||
|
||||
stopPropagation: (event) ->
|
||||
|
@ -62,8 +131,8 @@ class App.SearchableSelect extends Spine.Controller
|
|||
switch event.keyCode
|
||||
when 40 then @nudge event, 1 # down
|
||||
when 38 then @nudge event, -1 # up
|
||||
when 39 then @fillWithAutocompleteSuggestion event # right
|
||||
when 37 then @fillWithAutocompleteSuggestion event # left
|
||||
when 39 then @autocompleteOrNavigateIn event # right
|
||||
when 37 then @autocompleteOrNavigateOut event # left
|
||||
when 13 then @onEnter event
|
||||
when 27 then @onEscape()
|
||||
when 9 then @onTab event
|
||||
|
@ -71,12 +140,20 @@ class App.SearchableSelect extends Spine.Controller
|
|||
onEscape: ->
|
||||
@toggle() if @isOpen
|
||||
|
||||
getCurrentOptions: ->
|
||||
@currentMenu.find('.js-option, .js-enter, .js-back')
|
||||
|
||||
getOptionIndex: (menu, value) ->
|
||||
menu.find('.js-option, .js-enter').filter("[data-value=\"#{value}\"]").index()
|
||||
|
||||
nudge: (event, direction) ->
|
||||
return @toggle() if not @isOpen
|
||||
|
||||
options = @getCurrentOptions()
|
||||
|
||||
event.preventDefault()
|
||||
visibleOptions = @option_items.not('.is-hidden')
|
||||
highlightedItem = @option_items.filter('.is-active')
|
||||
visibleOptions = options.not('.is-hidden')
|
||||
highlightedItem = options.filter('.is-active')
|
||||
currentPosition = visibleOptions.index(highlightedItem)
|
||||
|
||||
currentPosition += direction
|
||||
|
@ -84,10 +161,24 @@ class App.SearchableSelect extends Spine.Controller
|
|||
return if currentPosition < 0
|
||||
return if currentPosition > visibleOptions.size() - 1
|
||||
|
||||
@option_items.removeClass('is-active')
|
||||
visibleOptions.eq(currentPosition).addClass('is-active')
|
||||
@unhighlightCurrentItem()
|
||||
@currentItem = visibleOptions.eq(currentPosition)
|
||||
@currentItem.addClass('is-active')
|
||||
@clearAutocomplete()
|
||||
|
||||
autocompleteOrNavigateIn: (event) ->
|
||||
if @currentItem.hasClass('js-enter')
|
||||
@navigateIn(event)
|
||||
else
|
||||
@fillWithAutocompleteSuggestion(event)
|
||||
|
||||
autocompleteOrNavigateOut: (event) ->
|
||||
# if we're in a depth then navigateOut
|
||||
if @level != 0
|
||||
@navigateOut(event)
|
||||
else
|
||||
@fillWithAutocompleteSuggestion(event)
|
||||
|
||||
fillWithAutocompleteSuggestion: (event) ->
|
||||
if !@suggestion
|
||||
return
|
||||
|
@ -129,11 +220,96 @@ class App.SearchableSelect extends Spine.Controller
|
|||
@shadowInput.val event.currentTarget.getAttribute('data-value')
|
||||
@shadowInput.trigger('change')
|
||||
|
||||
navigateIn: (event) ->
|
||||
event.stopPropagation()
|
||||
@navigateDepth(1)
|
||||
|
||||
navigateOut: (event) ->
|
||||
event.stopPropagation()
|
||||
@navigateDepth(-1)
|
||||
|
||||
navigateDepth: (dir) ->
|
||||
return if @animating
|
||||
if dir > 0
|
||||
target = @currentItem.attr('data-value')
|
||||
target_menu = @optionsSubmenu.filter("[data-parent-value=\"#{target}\"]")
|
||||
else
|
||||
target_menu = @findMenuContainingValue @currentMenu.attr('data-parent-value')
|
||||
|
||||
@animateToSubmenu(target_menu, dir)
|
||||
|
||||
@level+=dir
|
||||
|
||||
animateToSubmenu: (target_menu, direction) ->
|
||||
@animating = true
|
||||
target_menu.prop('hidden', false)
|
||||
@dropdown.height(Math.max(target_menu.height(), @currentMenu.height()))
|
||||
oldCurrentItem = @currentItem
|
||||
|
||||
@currentMenu.data('current_item_index', @currentItem.index())
|
||||
# default: 1 (first item after the back button)
|
||||
target_item_index = target_menu.data('current_item_index') || 1
|
||||
# if the direction is out then we know the target item -> its the parent item
|
||||
if direction is -1
|
||||
value = @currentMenu.attr('data-parent-value')
|
||||
target_item_index = @getOptionIndex(target_menu, value)
|
||||
|
||||
@currentItem = target_menu.children().eq(target_item_index)
|
||||
@currentItem.addClass('is-active')
|
||||
|
||||
target_menu.velocity
|
||||
properties:
|
||||
translateX: [0, direction*100+'%']
|
||||
options:
|
||||
duration: 240
|
||||
|
||||
@currentMenu.velocity
|
||||
properties:
|
||||
translateX: [direction*-100+'%', 0]
|
||||
options:
|
||||
duration: 240
|
||||
complete: =>
|
||||
oldCurrentItem.removeClass('is-active')
|
||||
$.Velocity.hook(@currentMenu, 'translateX', '')
|
||||
@currentMenu.prop('hidden', true)
|
||||
@dropdown.height(target_menu.height())
|
||||
@currentMenu = target_menu
|
||||
@animating = false
|
||||
|
||||
showSubmenu: (menu) ->
|
||||
@currentMenu.prop('hidden', true)
|
||||
menu.prop('hidden', false)
|
||||
@dropdown.height(menu.height())
|
||||
|
||||
findMenuContainingValue: (value) ->
|
||||
return @optionsList if !value
|
||||
|
||||
# in case of numbers
|
||||
if !value.split && value.toString
|
||||
value = value.toString()
|
||||
path = value.split('::')
|
||||
if path.length == 1
|
||||
return @optionsList
|
||||
else
|
||||
path.pop()
|
||||
return @optionsSubmenu.filter("[data-parent-value=\"#{path.join('::')}\"]")
|
||||
|
||||
getIndex: (menu) ->
|
||||
parentValue = menu.attr('data-parent-value')
|
||||
return 0 if !parentValue
|
||||
return parentValue.split('::').length
|
||||
|
||||
onTab: (event) ->
|
||||
return if not @isOpen
|
||||
event.preventDefault()
|
||||
|
||||
onEnter: (event) ->
|
||||
if @currentItem
|
||||
if @currentItem.hasClass('js-enter')
|
||||
return @navigateIn(event)
|
||||
else if @currentItem.hasClass('js-back')
|
||||
return @navigateOut(event)
|
||||
|
||||
@clearAutocomplete()
|
||||
|
||||
if not @isOpen
|
||||
|
@ -144,13 +320,14 @@ class App.SearchableSelect extends Spine.Controller
|
|||
|
||||
event.preventDefault()
|
||||
|
||||
selected = @option_items.filter('.is-active')
|
||||
if selected.length || !@options.attribute.unknown
|
||||
valueName = selected.text().trim()
|
||||
value = selected.attr('data-value')
|
||||
if @currentItem || !@attribute.unknown
|
||||
valueName = @currentItem.text().trim()
|
||||
value = @currentItem.attr('data-value')
|
||||
@input.val valueName
|
||||
@shadowInput.val value
|
||||
|
||||
@currentItem = null
|
||||
|
||||
@input.trigger('change')
|
||||
@shadowInput.trigger('change')
|
||||
@toggle()
|
||||
|
@ -169,32 +346,46 @@ class App.SearchableSelect extends Spine.Controller
|
|||
@query = @input.val()
|
||||
@filterByQuery @query
|
||||
|
||||
if @options.attribute.unknown
|
||||
if @attribute.unknown
|
||||
@shadowInput.val @query
|
||||
|
||||
filterByQuery: (query) ->
|
||||
query = escapeRegExp(query)
|
||||
regex = new RegExp(query.split(' ').join('.*'), 'i')
|
||||
|
||||
@option_items
|
||||
@optionsList.addClass 'is-filtered'
|
||||
|
||||
@optionItems
|
||||
.addClass 'is-hidden'
|
||||
.filter ->
|
||||
@textContent.match(regex)
|
||||
.removeClass 'is-hidden'
|
||||
|
||||
if @options.attribute.unknown && @option_items.length == @option_items.filter('.is-hidden').length
|
||||
@option_items.removeClass 'is-hidden'
|
||||
@option_items.removeClass 'is-active'
|
||||
if !query
|
||||
@optionItems.filter('.is-child').addClass 'is-hidden'
|
||||
|
||||
# if all are hidden
|
||||
if @attribute.unknown && @optionItems.length == @optionItems.filter('.is-hidden').length
|
||||
@optionItems.not('.is-child').removeClass 'is-hidden'
|
||||
@unhighlightCurrentItem()
|
||||
@optionsList.removeClass 'is-filtered'
|
||||
else
|
||||
@highlightFirst(true)
|
||||
|
||||
highlightFirst: (autocomplete) ->
|
||||
first = @option_items.removeClass('is-active').not('.is-hidden').first()
|
||||
first.addClass 'is-active'
|
||||
@unhighlightCurrentItem()
|
||||
@currentItem = @getCurrentOptions().not('.is-hidden').first()
|
||||
@currentItem.addClass 'is-active'
|
||||
|
||||
if autocomplete
|
||||
@autocomplete first.attr('data-value'), first.text().trim()
|
||||
@autocomplete @currentItem.attr('data-value'), @currentItem.text().trim()
|
||||
|
||||
highlightItem: (event) =>
|
||||
@option_items.removeClass('is-active')
|
||||
$(event.currentTarget).addClass('is-active')
|
||||
@unhighlightCurrentItem()
|
||||
@currentItem = $(event.currentTarget)
|
||||
@currentItem.addClass('is-active')
|
||||
|
||||
unhighlightCurrentItem: ->
|
||||
return if !@currentItem
|
||||
@currentItem.removeClass('is-active')
|
||||
@currentItem = null
|
||||
|
|
|
@ -70,8 +70,7 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
|
|||
options.push data
|
||||
|
||||
# fill template with gathered options
|
||||
@optionsList.html App.view('generic/searchable_select_options')
|
||||
options: options
|
||||
@optionsList.html @renderOptions options
|
||||
|
||||
# refresh elements
|
||||
@refreshElements()
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
<div class="dropdown-toggle" data-toggle="dropdown">
|
||||
<input
|
||||
class="searchableSelect-shadow form-control js-shadow"
|
||||
id="<%= @id %>"
|
||||
name="<%= @name %>"
|
||||
<%= @required %>
|
||||
<%= @autofocus %>
|
||||
value="<%= @value %>"
|
||||
id="<%= @attribute.id %>"
|
||||
name="<%= @attribute.name %>"
|
||||
<%= @attribute.required %>
|
||||
<%= @attribute.autofocus %>
|
||||
value="<%= @attribute.value %>"
|
||||
>
|
||||
<input
|
||||
class="searchableSelect-main form-control js-input<%= " #{ @class }" if @class %>"
|
||||
placeholder="<%= @placeholder %>"
|
||||
value="<%= @valueName %>"
|
||||
class="searchableSelect-main form-control js-input<%= " #{ @attribute.class }" if @attribute.class %>"
|
||||
placeholder="<%= @attribute.placeholder %>"
|
||||
value="<%= @attribute.valueName %>"
|
||||
autocomplete="off"
|
||||
<%= @required %>
|
||||
<%= @attribute.required %>
|
||||
>
|
||||
<div class="searchableSelect-autocomplete">
|
||||
<span class="searchableSelect-autocomplete-invisible js-autocomplete-invisible"></span>
|
||||
<span class="searchableSelect-autocomplete-visible js-autocomplete-visible"></span>
|
||||
</div>
|
||||
<% if !@ajax: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %>
|
||||
<% if !@attribute.ajax: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %>
|
||||
<div class="small loading icon"></div>
|
||||
</div>
|
||||
<ul class="dropdown-menu dropdown-menu-left js-optionsList" role="menu">
|
||||
<%- @renderedOptions %>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu--has-submenu js-dropdown">
|
||||
<ul class="js-optionsList" role="menu">
|
||||
<%- @options %>
|
||||
</ul>
|
||||
<%- @submenus %>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<li role="presentation" class="<%= @class %>" data-value="<%= @option.value %>" title="<%= @option.name %><% if @detail: %><%= @detail %><% end %>">
|
||||
<span class="searchableSelect-option-text">
|
||||
<%= @option.name %><% if @detail: %><span class="dropdown-detail"><%= @detail %></span><% end %>
|
||||
</span>
|
||||
<% if @option.children: %>
|
||||
<%- @Icon('arrow-right', 'recipientList-arrow') %>
|
||||
<% end %>
|
||||
</li>
|
|
@ -1,5 +0,0 @@
|
|||
<% if @options: %>
|
||||
<% for option in @options: %>
|
||||
<li role="presentation" class="js-option" data-value="<%= option.value %>"><%= option.name %>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -0,0 +1,11 @@
|
|||
<ul class="dropdown-submenu js-optionsSubmenu" role="menu" data-parent-value="<%- @parentValue %>" hidden>
|
||||
<% if @title: %>
|
||||
<li class="dropdown-controls js-back">
|
||||
<%- @Icon('arrow-left') %>
|
||||
<div class="dropdown-title">
|
||||
<%- @title %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<%- @options %>
|
||||
</ul>
|
|
@ -0,0 +1,29 @@
|
|||
<div>
|
||||
<table class="settings-list js-Table" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%- @T('Key') %>
|
||||
<th style="width: 180px"><%- @T('Action') %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="js-treeTable"></tbody>
|
||||
</table>
|
||||
<table class="hidden">
|
||||
<tbody>
|
||||
<tr class="js-template">
|
||||
<td class="settings-list-control-cell">
|
||||
<input class="form-control form-control--small js-key" type="text" value="" data-level="" required/>
|
||||
<td class="settings-list-row-control">
|
||||
<div class="btn btn--text js-remove" style="margin-left: -10px;">
|
||||
<%- @Icon('trash') %>
|
||||
</div>
|
||||
<div class="btn btn--text btn--create js-addChild" style="margin-left: -10px;">
|
||||
<%- @Icon('plus-small') %> <%- @T('children') %>
|
||||
</div>
|
||||
<div class="btn btn--text btn--create js-addRow" style="margin-left: -10px;">
|
||||
<%- @Icon('plus-small') %> <%- @T('row') %>
|
||||
</div>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -6,6 +6,7 @@
|
|||
<div class="js-datetime">date time settings</div>
|
||||
<div class="js-date">date settings</div>
|
||||
<div class="js-select">select settings</div>
|
||||
<div class="js-tree_selection">tree selection settings</div>
|
||||
<div class="js-checkbox">checkbox settings</div>
|
||||
<div class="js-boolean">boolean settings</div>
|
||||
<div class="js-richtext">richtext settings</div>
|
||||
|
|
|
@ -1396,6 +1396,7 @@ fieldset > .form-group {
|
|||
.merge-target,
|
||||
.merge-source {
|
||||
flex: 1;
|
||||
width: 33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
@ -6130,6 +6131,48 @@ footer {
|
|||
height: 30px;
|
||||
}
|
||||
|
||||
.dropdown-menu--has-submenu {
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
|
||||
ul {
|
||||
background: hsl(234,10%,19%);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-submenu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown.dropdown--actions .dropdown-controls {
|
||||
@extend .u-clickable;
|
||||
display: flex;
|
||||
|
||||
&:not(:hover):not(.is-active) {
|
||||
background: hsl(206,7%,28%);
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: white;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-detail {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.recipientList,
|
||||
.recipientList-organizationMembers {
|
||||
list-style: none;
|
||||
|
@ -7493,15 +7536,31 @@ output {
|
|||
|
||||
.dropdown-menu {
|
||||
margin-top: -3px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.dropdown li:hover:not(.is-active) {
|
||||
&-option-text {
|
||||
flex: 1 1 0%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
|
||||
& + .icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.dropdown li {
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.dropdown li.is-hidden {
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
li:not(.is-active):hover + li {
|
||||
box-shadow: 0 1px rgba(255,255,255,.13) inset;
|
||||
|
|
|
@ -108,7 +108,7 @@ class ObjectManagerAttributesController < ApplicationController
|
|||
end
|
||||
end
|
||||
if params[:data_option] && !params[:data_option].key?(:default)
|
||||
params[:data_option][:default] = if params[:data_type] =~ /^(input|select)$/
|
||||
params[:data_option][:default] = if params[:data_type] =~ /^(input|select|tree_select)$/
|
||||
''
|
||||
end
|
||||
end
|
||||
|
|
|
@ -115,6 +115,57 @@ possible types
|
|||
note: 'some additional comment', # optional
|
||||
},
|
||||
|
||||
# tree_select
|
||||
|
||||
data_type: 'tree_select',
|
||||
data_option: {
|
||||
default: 'aa',
|
||||
options: [
|
||||
{
|
||||
'value' => 'aa',
|
||||
'name' => 'aa (comment)',
|
||||
'children' => [
|
||||
{
|
||||
'value' => 'aaa',
|
||||
'name' => 'aaa (comment)',
|
||||
},
|
||||
{
|
||||
'value' => 'aab',
|
||||
'name' => 'aab (comment)',
|
||||
},
|
||||
{
|
||||
'value' => 'aac',
|
||||
'name' => 'aac (comment)',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'value' => 'bb',
|
||||
'name' => 'bb (comment)',
|
||||
'children' => [
|
||||
{
|
||||
'value' => 'bba',
|
||||
'name' => 'aaa (comment)',
|
||||
},
|
||||
{
|
||||
'value' => 'bbb',
|
||||
'name' => 'bbb (comment)',
|
||||
},
|
||||
{
|
||||
'value' => 'bbc',
|
||||
'name' => 'bbc (comment)',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
maxlength: 200,
|
||||
nulloption: true,
|
||||
null: false,
|
||||
multiple: false, # currently only "false" supported
|
||||
translate: true, # optional
|
||||
note: 'some additional comment', # optional
|
||||
},
|
||||
|
||||
# checkbox
|
||||
|
||||
data_type: 'checkbox',
|
||||
|
@ -550,7 +601,7 @@ to send no browser reload event, pass false
|
|||
end
|
||||
|
||||
data_type = nil
|
||||
if attribute.data_type =~ /^input|select|richtext|textarea|checkbox$/
|
||||
if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/
|
||||
data_type = :string
|
||||
elsif attribute.data_type =~ /^integer|user_autocompletion$/
|
||||
data_type = :integer
|
||||
|
@ -564,7 +615,7 @@ to send no browser reload event, pass false
|
|||
|
||||
# change field
|
||||
if model.column_names.include?(attribute.name)
|
||||
if attribute.data_type =~ /^input|select|richtext|textarea|checkbox$/
|
||||
if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/
|
||||
ActiveRecord::Migration.change_column(
|
||||
model.table_name,
|
||||
attribute.name,
|
||||
|
@ -603,7 +654,7 @@ to send no browser reload event, pass false
|
|||
end
|
||||
|
||||
# create field
|
||||
if attribute.data_type =~ /^input|select|richtext|textarea|checkbox$/
|
||||
if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/
|
||||
ActiveRecord::Migration.add_column(
|
||||
model.table_name,
|
||||
attribute.name,
|
||||
|
@ -704,7 +755,7 @@ to send no browser reload event, pass false
|
|||
if !data_type
|
||||
raise 'Need data_type param'
|
||||
end
|
||||
if data_type !~ /^(input|user_autocompletion|checkbox|select|datetime|date|tag|richtext|textarea|integer|autocompletion_ajax|boolean|user_permission|active)$/
|
||||
if data_type !~ /^(input|user_autocompletion|checkbox|select|tree_select|datetime|date|tag|richtext|textarea|integer|autocompletion_ajax|boolean|user_permission|active)$/
|
||||
raise "Invalid data_type param '#{data_type}'"
|
||||
end
|
||||
|
||||
|
@ -735,7 +786,7 @@ to send no browser reload event, pass false
|
|||
}
|
||||
end
|
||||
|
||||
if data_type == 'select' || data_type == 'checkbox'
|
||||
if data_type == 'select' || data_type == 'tree_select' || data_type == 'checkbox'
|
||||
raise 'Need data_option[:default] param' if !data_option.key?(:default)
|
||||
raise 'Invalid data_option[:options] or data_option[:relation] param' if data_option[:options].nil? && data_option[:relation].nil?
|
||||
if !data_option.key?(:maxlength)
|
||||
|
|
22
app/views/tests/form_tree_select.html.erb
Normal file
22
app/views/tests/form_tree_select.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
|
||||
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
|
||||
<script src="/assets/tests/qunit-1.21.0.js"></script>
|
||||
<script src="/assets/tests/form_tree_select.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
|
||||
<div id="qunit" class="u-dontfold"></div>
|
||||
|
||||
<div>
|
||||
<form class="form-stacked pull-left">
|
||||
<div id="forms"></div>
|
||||
<button type="submit" class="btn btn-primary submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -5,6 +5,7 @@ Zammad::Application.routes.draw do
|
|||
match '/tests_model', to: 'tests#model', via: :get
|
||||
match '/tests_model_ui', to: 'tests#model_ui', via: :get
|
||||
match '/tests_form', to: 'tests#form', via: :get
|
||||
match '/tests_form_tree_select', to: 'tests#form_tree_select', via: :get
|
||||
match '/tests_form_find', to: 'tests#form_find', via: :get
|
||||
match '/tests_form_trim', to: 'tests#form_trim', via: :get
|
||||
match '/tests_form_extended', to: 'tests#form_extended', via: :get
|
||||
|
|
|
@ -563,8 +563,8 @@ class CreateBase < ActiveRecord::Migration
|
|||
t.string :name, limit: 200, null: false
|
||||
t.string :display, limit: 200, null: false
|
||||
t.string :data_type, limit: 100, null: false
|
||||
t.string :data_option, limit: 8000, null: true
|
||||
t.string :data_option_new, limit: 8000, null: true
|
||||
t.text :data_option, limit: 800.kilobytes + 1, null: true
|
||||
t.text :data_option_new, limit: 800.kilobytes + 1, null: true
|
||||
t.boolean :editable, null: false, default: true
|
||||
t.boolean :active, null: false, default: true
|
||||
t.string :screens, limit: 2000, null: true
|
||||
|
|
6
db/migrate/20170619000001_tree_select.rb
Normal file
6
db/migrate/20170619000001_tree_select.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class TreeSelect < ActiveRecord::Migration
|
||||
def up
|
||||
change_column :object_manager_attributes, :data_option, :text, limit: 800.kilobytes + 1, null: true
|
||||
change_column :object_manager_attributes, :data_option_new, :text, limit: 800.kilobytes + 1, null: true
|
||||
end
|
||||
end
|
194
public/assets/tests/form_tree_select.js
Normal file
194
public/assets/tests/form_tree_select.js
Normal file
|
@ -0,0 +1,194 @@
|
|||
test("form elements check", function() {
|
||||
$('#forms').append('<hr><h1>form elements check</h1><form id="form1"></form>')
|
||||
var el = $('#form1')
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: {
|
||||
"configure_attributes": [
|
||||
{
|
||||
"name": "tree_select",
|
||||
"display": "tree_select",
|
||||
"tag": "tree_select",
|
||||
"null": true,
|
||||
"translate": true,
|
||||
"options": [
|
||||
{
|
||||
"value": "aa",
|
||||
"name": "yes",
|
||||
"children": [
|
||||
{
|
||||
"value": "aa::aaa",
|
||||
"name": "yes1",
|
||||
},
|
||||
{
|
||||
"value": "aa::aab",
|
||||
"name": "yes2",
|
||||
},
|
||||
{
|
||||
"value": "aa::aac",
|
||||
"name": "yes3",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "bb",
|
||||
"name": "bb (comment)",
|
||||
"children": [
|
||||
{
|
||||
"value": "bb::bba",
|
||||
"name": "yes11",
|
||||
},
|
||||
{
|
||||
"value": "bb::bbb",
|
||||
"name": "yes22",
|
||||
},
|
||||
{
|
||||
"value": "bb::bbc",
|
||||
"name": "yes33",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
autofocus: true
|
||||
});
|
||||
equal(el.find('[name="tree_select"]').val(), '', 'check tree_select value');
|
||||
equal(el.find('[name="tree_select"]').closest('.searchableSelect').find('.js-input').val(), '', 'check tree_select .js-input value');
|
||||
var params = App.ControllerForm.params(el)
|
||||
var test_params = {
|
||||
tree_select: ''
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
$('#forms').append('<hr><h1>form elements check</h1><form id="form2"></form>')
|
||||
var el = $('#form2')
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: {
|
||||
"configure_attributes": [
|
||||
{
|
||||
"name": "tree_select",
|
||||
"display": "tree_select",
|
||||
"tag": "tree_select",
|
||||
"null": true,
|
||||
"translate": true,
|
||||
"value": "aa",
|
||||
"options": [
|
||||
{
|
||||
"value": "aa",
|
||||
"name": "yes",
|
||||
"children": [
|
||||
{
|
||||
"value": "aa::aaa",
|
||||
"name": "yes1",
|
||||
},
|
||||
{
|
||||
"value": "aa::aab",
|
||||
"name": "yes2",
|
||||
},
|
||||
{
|
||||
"value": "aa::aac",
|
||||
"name": "yes3",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "bb",
|
||||
"name": "bb (comment)",
|
||||
"children": [
|
||||
{
|
||||
"value": "bb::bba",
|
||||
"name": "yes11",
|
||||
},
|
||||
{
|
||||
"value": "bb::bbb",
|
||||
"name": "yes22",
|
||||
},
|
||||
{
|
||||
"value": "bb::bbc",
|
||||
"name": "yes33",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
autofocus: true
|
||||
});
|
||||
|
||||
equal(el.find('[name="tree_select"]').val(), 'aa', 'check tree_select value');
|
||||
equal(el.find('[name="tree_select"]').closest('.searchableSelect').find('.js-input').val(), 'yes', 'check tree_select .js-input value');
|
||||
var params = App.ControllerForm.params(el)
|
||||
var test_params = {
|
||||
tree_select: 'aa'
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
$('#forms').append('<hr><h1>form elements check</h1><form id="form3"></form>')
|
||||
var el = $('#form3')
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: {
|
||||
"configure_attributes": [
|
||||
{
|
||||
"name": "tree_select",
|
||||
"display": "tree_select",
|
||||
"tag": "tree_select",
|
||||
"null": true,
|
||||
"translate": true,
|
||||
"value": "aa::aab",
|
||||
"options": [
|
||||
{
|
||||
"value": "aa",
|
||||
"name": "yes",
|
||||
"children": [
|
||||
{
|
||||
"value": "aa::aaa",
|
||||
"name": "yes1",
|
||||
},
|
||||
{
|
||||
"value": "aa::aab",
|
||||
"name": "yes2",
|
||||
},
|
||||
{
|
||||
"value": "aa::aac",
|
||||
"name": "yes3",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "bb",
|
||||
"name": "bb (comment)",
|
||||
"children": [
|
||||
{
|
||||
"value": "bb::bba",
|
||||
"name": "yes11",
|
||||
},
|
||||
{
|
||||
"value": "bb::bbb",
|
||||
"name": "yes22",
|
||||
},
|
||||
{
|
||||
"value": "bb::bbc",
|
||||
"name": "yes33",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
autofocus: true
|
||||
});
|
||||
equal(el.find('[name="tree_select"]').val(), 'aa::aab', 'check tree_select value');
|
||||
equal(el.find('[name="tree_select"]').closest('.searchableSelect').find('.js-input').val(), 'yes2', 'check tree_select .js-input value');
|
||||
var params = App.ControllerForm.params(el)
|
||||
var test_params = {
|
||||
tree_select: 'aa::aab'
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
});
|
|
@ -87,6 +87,13 @@ class AAbUnitTest < TestCase
|
|||
value: '0',
|
||||
)
|
||||
|
||||
location(url: browser_url + '/tests_form_tree_select')
|
||||
sleep 2
|
||||
match(
|
||||
css: '.result .failed',
|
||||
value: '0',
|
||||
)
|
||||
|
||||
location(url: browser_url + '/tests_form_column_select')
|
||||
sleep 2
|
||||
match(
|
||||
|
|
Loading…
Reference in a new issue