"""Vertically aligned list of customizable cards with items that can be moved, added and removed."""
# Author(s): Davide.De-Marchi@ec.europa.eu
# Copyright © European Union 2022-2023
#
# Licensed under the EUPL, Version 1.2 or as soon they will be approved by
# the European Commission subsequent versions of the EUPL (the "Licence");
#
# You may not use this work except in compliance with the Licence.
#
# You may obtain a copy of the Licence at:
# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licence is distributed on an "AS IS"
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied.
#
# See the Licence for the specific language governing permissions and
# limitations under the Licence.
from ipywidgets import widgets, Layout
from IPython.display import display
import ipyvuetify as v
try:
from . import settings
from . import tooltip
except:
import settings
import tooltip
#####################################################################################################################################################
# Vertically aligned list of customizable cards with items that can be moved, added and removed.
#####################################################################################################################################################
[docs]class sortableList:
"""
Vertically aligned list of customizable cards with items that can be moved, added and removed.
Parameters
----------
items : list of dicts, optional
List of items to display in the list (default is [])
width : int, optional
Width of widget in pixels (default is 400)
outlined : bool, optional
Flag to show each of the items with a border (default is True)
dark : bool, optional
Flag to invert the text and backcolor (default is the value of settings.dark_mode)
allowNew : bool, optional
If True, a 'plus' button is displayed that allows for adding new items (default is True)
newOnTop: bool, optional
If True, the '+' button adds a new item in first position on the items list (default False, new items are added as last in the items list)
newButtonOnTop: bool, optional
If True, the '+' button is displayed on top of the first item, otherwise it is displayed below the last item (default is False, the '+' button is on the bottom)
itemNew : function, optional
Python function called when a new items is added. The function is called with no arguments and it must return the dict initialized with the new item content (default is None). As an alternative, the function can return None, but then the real adding of the new item must be done by directly calling the doAddItem method
itemContent : function, optional
Python function called when an item is displayed. The function is called with an item as its first argument and the index (position of the item) as second argument. The function must return a list containing the ipyvuetify widgets to display the item content (default is None)
bottomContent : list of ipyvuetify widgets, optional
Additional widgets content to display in the bottom line (containing the 'plus' button), aligned to the right (default is [])
onchange : function, optional
Python function to call when the user changes the order of items or removes an item. The function will receive no parameters as input (default is None)
onmovedown : function, optional
Python function to call when the user moves one item down. The function will receive as parameter the zero-based index, before the move, of the moved item (default is None)
onmoveup : function, optional
Python function to call when the user moves one item up. The function will receive as parameter the zero-based index, before the move, of the moved item (default is None)
onremoving : function, optional
Python function to call when the user is about to remove an item. The function will receive as parameter the zero-based index of the item that is going to be removed (default is None)
onremoved : function, optional
Python function to call just after the user removes an item. The function will receive as parameter the zero-based index of the removed item (default is None)
onadded : function, optional
Python function to call just after a new item is added. The function will receive as parameter the zero-based index of the new item (default is None)
buttonstooltip : bool, optional
If True, the buttons to mode, add, remove items will have a tooltip (default is False)
tooltipadd : str, optional
Tooltip text for the "add" button (default is 'Add new')
tooltipdown : str, optional
Tooltip text for the "move down" buttons (default is 'Move down')
tooltipup : str, optional
Tooltip text for the "move up" buttons (default is 'Move up')
tooltipremove : str, optional
Tooltip text for the "remove" buttons (default is 'Remove')
activatable : bool, optional
If True the items can be activated by clicking on them (default is False)
ondeactivated : function, optional
Python function to call just after an item that was the active one, is deactivated. The function will receive as parameter the zero-based index of the deactivated item (default is None)
onactivated : function, optional
Python function to call just after an item becomes the active one (or by user-clicking or by setting the active property). The function will receive as parameter the zero-based index of the active item (default is None)
Examples
--------
Simple list displaying static text::
from vois.vuetify import sortableList
from ipywidgets import widgets
from IPython.display import display
import ipyvuetify as v
items = [{ "name": 'Jane Adams', "email": 'jane@adams.com' },
{ "name": 'Paul Davis', "email": 'paul@davis.com' },
{ "name": 'Amanda Brown', "email": 'amanda@brown.com' }
]
# Creation of a new item
def itemNew():
return {"name": "new", "email": "empty"}
# Content of an item
def itemContent(item, index):
return [
v.CardSubtitle(class_="mb-n4", children=[item['name']]),
v.CardText( class_="mt-n2", children=[item['email']])
]
s = sortableList.sortableList(items=items, dark=False, allowNew=True,
itemNew=itemNew, itemContent=itemContent)
display(s.draw())
.. figure:: figures/sortableList1.png
:scale: 100 %
:alt: label widget
Example of a simple sortableList displaying static text
Example of a sortable list displaying editable text, boolean and date values (using the :py:class:`datePicker.datePicker` class)::
from vois.vuetify import sortableList, datePicker, switch, tooltip
from ipywidgets import widgets
from IPython.display import display
import ipyvuetify as v
output = widgets.Output()
items = [
{ "name": "Paul", "surname": "Dockery", "married": False, "date": "" },
{ "name": "July", "surname": "Winters", "married": True, "date": "1997-07-28" },
{ "name": "David", "surname": "Forest", "married": True, "date": "1999-03-03" },
{ "name": "Dorothy", "surname": "Landmann", "married": False, "date": "" }
]
dark = False
# Called when an item is moved or deleted
def onchange():
with output:
print('Changed!')
# Creation of a new item
def itemNew():
return { "name": "", "surname": "", "married": False, "date": "" }
# Remove all items
def itemRemoveAll(widget, event, data):
s.items = []
reset = v.Btn(icon=True, children=[v.Icon(children=['mdi-playlist-remove'])])
reset.on_event('click', itemRemoveAll)
# Content of an item
def itemContent(item, index):
def onname(widget, event, data):
item["name"] = int(data)
def onsurname(widget, event, data):
item["surname"] = data
def onmarried(flag):
item["married"] = flag
dp.disabled = not flag
if not flag:
item["date"] = ''
dp.date = None
def ondate():
item["date"] = dp.date
tfname = v.TextField(label='Name:', value=item['name'],
color='amber', dense=True,
style_="max-width: 70px", class_="pa-0 ma-0 mt-2")
tfname.on_event('input', onname)
tfsurname = v.TextField(label='Surname:', value=item['surname'],
color='amber', dense=True,
style_="max-width: 100px", class_="pa-0 ma-0 mt-2")
tfsurname.on_event('input', onsurname)
sw = switch.switch(item['married'], "Married", onchange=onmarried)
dp = datePicker.datePicker(date=item['date'], dark=dark, width=88,
onchange=ondate, offset_x=True, offset_y=False)
dp.disabled = not item['married']
sp = v.Html(tag='div', class_="pa-0 ma-0 mr-3", children=[''])
return [ v.Row(class_="pa-0 ma-0 ml-2", no_gutters=True,
children=[tfname, sp, tfsurname, sp, sw.draw(), sp, dp.draw()]) ]
s = sortableList.sortableList(items=items,
width=520,
outlined=False,
dark=dark,
allowNew=True,
itemNew=itemNew,
itemContent=itemContent,
bottomContent=[tooltip.tooltip("Remove all persons",
reset)],
onchange=onchange,
buttonstooltip=True)
display(s.draw())
display(output)
.. figure:: figures/sortableList2.png
:scale: 100 %
:alt: label widget
Example of a sortableList to edit textual, boolean and date values on persons.
"""
# Initialization
def __init__(self, items=[], width=400, maxheightlist=10000, outlined=True, dark=settings.dark_mode,
allowNew=True, newOnTop=False, newButtonOnTop=False, itemNew=None, itemContent=None, bottomContent=[],
onchange=None, onmovedown=None, onmoveup=None, onremoving=None, onremoved=None, onadded=None, buttonstooltip=False,
tooltipadd='Add new', tooltipdown='Move down', tooltipup='Move up', tooltipremove='Remove',
activatable=False, ondeactivated=None, onactivated=None):
self._items = items
self.width = width
self.outlined = outlined
self.dark = dark
self.allowNew = allowNew
self.newOnTop = newOnTop
self.newButtonOnTop = newButtonOnTop
self.itemNew = itemNew
self.itemContent = itemContent
self.bottomContent = bottomContent
self.onchange = onchange
self.buttonstooltip = buttonstooltip
self.tooltipadd = tooltipadd
self.tooltipdown = tooltipdown
self.tooltipup = tooltipup
self.tooltipremove = tooltipremove
self.onmovedown = onmovedown
self.onmoveup = onmoveup
self.onremoving = onremoving
self.onremoved = onremoved
self.onadded = onadded
self.activatable = activatable
self.ondeactivated = ondeactivated
self.onactivated = onactivated
self.activeindex = -1
self.output = v.Card(flat=True, max_height='%dpx'%maxheightlist, children=[])
self.outputplus = v.Card(flat=True, children=[])
self.cards = []
self.bdowns = []
self.bups = []
self.bremoves = []
# Create the + button to add a new item
self.style = "min-width: %dpx; max-width: %dpx" %(self.width,self.width)
if self.allowNew:
self.plusbutton = v.Btn(icon=True, dark=self.dark, children=[v.Icon(children=['mdi-plus'])])
self.plusbutton.on_event('click', self.onadd)
if self.buttonstooltip:
self.outputplus.children = [v.Row(no_gutters=True, style_=self.style, justify='space-between',
children=[tooltip.tooltip(self.tooltipadd, self.plusbutton)] + self.bottomContent)]
else:
self.outputplus.children = [v.Row(no_gutters=True, style_=self.style, justify='space-between',
children=[self.plusbutton] + self.bottomContent)]
# Add all the items
for index, item in enumerate(self._items):
self.additem(item, index==0, index==len(self._items)-1)
if self.activatable: bottomspace = 'mb-2'
else: bottomspace = ''
self.col = v.Col(class_="pa-0 ma-0 %s"%bottomspace, children=self.cards)
self.output.children = [self.col]
# Click on + button
def onadd(self, widget, event, data):
if self.itemNew:
item = self.itemNew()
if not item is None:
self.doAddItem(item)
if not self.onadded is None:
if self.newOnTop:
self.onadded(0)
else:
self.onadded(len(self.cards)-1)
# Effective add of a new item
[docs] def doAddItem(self, item):
"""
Manual adding of a new item
"""
if self.newOnTop:
if len(self.bups) > 0: self.bups[0].disabled = False
self._items.insert(0,item)
if self.activeindex >= 0:
self.activeindex += 1
self.additem(item, True, len(self.cards)==0, onTop=True)
else:
if len(self.bdowns) > 0: self.bdowns[-1].disabled = False
self._items.append(item)
self.additem(item, len(self.cards)==0, True)
if self.activatable: bottomspace = 'mb-2'
else: bottomspace = ''
self.col = v.Col(class_="pa-0 ma-0 %s"%bottomspace, children=self.cards)
self.output.children = [self.col]
if self.onchange:
self.onchange()
# Update the disabled state of the buttons
def update_buttons(self):
for i in range(len(self.bdowns)): self.bdowns[i].disabled = False
if len(self.bdowns)>0: self.bdowns[-1].disabled = True
for i in range(len(self.bups)): self.bups[i].disabled = False
if len(self.bups)>0: self.bups[0].disabled = True
# Move item down
def ondown(self, widget, event, data):
index = self.bdowns.index(widget)
wasactive = False
if index == self.activeindex:
wasactive = True
changewithactive = False
if index+1 == self.activeindex:
changewithactive = True
a = index
b = index + 1
self.updateCard(self._items[a], a)
self.updateCard(self._items[b], b)
self._items[b], self._items[a] = self._items[a], self._items[b]
self.cards[b], self.cards[a] = self.cards[a], self.cards[b]
self.bdowns[b], self.bdowns[a] = self.bdowns[a], self.bdowns[b]
self.bups[b], self.bups[a] = self.bups[a], self.bups[b]
self.bremoves[b], self.bremoves[a] = self.bremoves[a], self.bremoves[b]
self.update_buttons()
if self.activatable: bottomspace = 'mb-2'
else: bottomspace = ''
self.col = v.Col(class_="pa-0 ma-0 %s"%bottomspace, children=self.cards)
self.output.children = [self.col]
if self.onchange:
self.onchange()
if self.onmovedown:
self.onmovedown(index)
if wasactive:
self.active = index + 1
elif changewithactive:
self.active = index
# Move item up
def onup(self, widget, event, data):
index = self.bups.index(widget)
wasactive = False
if index == self.activeindex:
wasactive = True
changewithactive = False
if index-1 == self.activeindex:
changewithactive = True
a = index - 1
b = index
self.updateCard(self._items[a], a)
self.updateCard(self._items[b], b)
self._items[b], self._items[a] = self._items[a], self._items[b]
self.cards[b], self.cards[a] = self.cards[a], self.cards[b]
self.bdowns[b], self.bdowns[a] = self.bdowns[a], self.bdowns[b]
self.bups[b], self.bups[a] = self.bups[a], self.bups[b]
self.bremoves[b], self.bremoves[a] = self.bremoves[a], self.bremoves[b]
self.update_buttons()
if self.activatable: bottomspace = 'mb-2'
else: bottomspace = ''
self.col = v.Col(class_="pa-0 ma-0 %s"%bottomspace, children=self.cards)
self.output.children = [self.col]
if self.onchange:
self.onchange()
if self.onmoveup:
self.onmoveup(index)
if wasactive:
self.active = index - 1
elif changewithactive:
self.active = index
# Remove item
def ondel(self, widget, event, data):
index = self.bremoves.index(widget)
if self.onremoving:
self.onremoving(index)
if index == self.activeindex:
if not self.ondeactivated is None:
self.ondeactivated(self.activeindex)
self.activeindex = -1
elif index <= self.activeindex:
self.activeindex -= 1
del self._items[index]
del self.cards[index]
del self.bdowns[index]
del self.bups[index]
del self.bremoves[index]
self.col.children = self.col.children[:index] + self.col.children[index+1 :]
self.update_buttons()
self.output.children = [self.col]
if self.onchange:
self.onchange()
if self.onremoved:
self.onremoved(index)
# Update the card for an item given its index
def updateCard(self, item, index):
if self.itemContent:
c = self.col.children[index]
c.children = [c.children[0]] + self.itemContent(item,index)
# Management of click on an item card
def __internal_onclick(self, widget=None, event=None, data=None):
if widget in self.cards:
index = self.cards.index(widget)
if self.activatable:
self.active = index
# Add an item to the lists
def additem(self, item, isfirst, islast, onTop=False):
if self.itemContent:
bdown = v.Btn(icon=True, class_="mr-n3", disabled=islast, children=[v.Icon(small=True, children=['mdi-arrow-down'])])
bup = v.Btn(icon=True, class_="mr-n3", disabled=isfirst, children=[v.Icon(small=True, children=['mdi-arrow-up'])])
bremove = v.Btn(icon=True, children=[v.Icon(small=True, children=['mdi-close'])])
if onTop:
self.bdowns.insert(0,bdown)
self.bups.insert(0,bup)
self.bremoves.insert(0,bremove)
else:
self.bdowns.append(bdown)
self.bups.append(bup)
self.bremoves.append(bremove)
bdown.on_event('click.stop', self.ondown)
bup.on_event('click.stop', self.onup)
bremove.on_event('click.stop', self.ondel)
if self.buttonstooltip:
buttons = [tooltip.tooltip(self.tooltipdown, bdown),
tooltip.tooltip(self.tooltipup, bup),
tooltip.tooltip(self.tooltipremove, bremove)]
else:
buttons = [bdown,bup,bremove]
if self.activatable: ripple = True
else: ripple = False
if onTop:
index = 0
else:
index = len(self.cards)
if self.activatable:
bottom = "mb-2"
else:
bottom = ""
c = v.Card(outlined=self.outlined, dark=self.dark, flat=True, dense=True, class_="pa-0 ma-0 mt-1 %s"%bottom, style_=self.style, ripple=ripple, raised=False,
children=[v.CardTitle(class_="justify-end pa-0 ma-0 mt-n1 mb-n5", children=buttons)] + self.itemContent(item,index))
c.on_event('click', self.__internal_onclick)
if onTop:
self.cards.insert(0,c)
else:
self.cards.append(c)
# Returns the vuetify object to display
[docs] def draw(self):
"""Returns the ipyvuetify object to display (a v.Html object displaying two output widgets)"""
if self.allowNew:
if self.newButtonOnTop:
return v.Html(tag='div',children=[widgets.VBox([self.outputplus,self.output])])
else:
return v.Html(tag='div',children=[widgets.VBox([self.output,self.outputplus])])
else:
return self.output
# Get the items
@property
def items(self):
"""
Get/Set the updated items.
Returns
--------
items : list of dicts
List of items in their updated position
"""
return self._items
# Set the items
@items.setter
def items(self, items):
self._items = items
self.cards = []
self.bdowns = []
self.bups = []
self.bremoves = []
self.activeindex = -1
# Add all the items
for index, item in enumerate(self._items):
self.additem(item, index==0, index==len(self._items)-1)
if self.activatable: bottomspace = 'mb-2'
else: bottomspace = ''
self.col = v.Col(class_="pa-0 ma-0 %s"%bottomspace, children=self.cards)
self.output.children = [self.col]
if self.onchange:
self.onchange()
# Get the index of the active item
@property
def active(self):
"""
Get/Set the active item.
Returns
--------
index : int
index of the active item
"""
return self.activeindex
# Set the active item
@active.setter
def active(self, index):
if index >= 0 and index < len(self.cards):
if self.activeindex >= 0:
self.cards[self.activeindex].raised = False
if not self.ondeactivated is None:
self.ondeactivated(self.activeindex)
self.activeindex = index
self.cards[self.activeindex].raised = True
if not self.onactivated is None:
self.onactivated(self.activeindex)