-
QuestionHi everybody. I'm looking for a generalized way to implement 'Custom Animated Drawers'. Here is the minimal example code : from nicegui import ui, context
ui.add_css('''
@keyframes fade_in_left {
from {
opacity: 0;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fade_out_left {
from {
opacity: 1;
transform: translateX(0px);
}
to {
opacity: 0;
transform: translateX(-100%);
}
}
''')
context.client.content.classes('h-[100vh] gap-0 m-0 p-0')
with ui.column().classes('w-full h-full gap-0 border-4 border-green-100').style("min-height:0"): # ensure that the main container resizes with the viewport
with ui.row():
ui.button(icon='add', on_click=lambda: mycol.style('animation: fade_in_left 0.25s ease-out forwards'))
ui.button(icon='remove', on_click=lambda: mycol.style('animation: fade_out_left 0.25s ease-in forwards'))
with ui.splitter().classes('w-full h-full border-4 border-red-100') as splitter:
with splitter.before:
with ui.column().classes('w-full h-full border-4 border-blue-100') as mycol:
ui.label('popo')
with splitter.after:
with ui.column().classes('w-full h-full border-4 border-yellow-100'):
ui.label('pupu')
ui.run()
Thanks in advance! |
Beta Was this translation helpful? Give feedback.
Replies: 6 comments 5 replies
-
Hi @ljilekor, This reminds me of #4034, which demonstrated something very similar just recently. |
Beta Was this translation helpful? Give feedback.
-
I altered the code from #4034 (thx @s71m !!) in an attempt to generalize drawers (or panels) to any div like element... So... In python, I added a custom attribute 'is_nested_drawer' to the div-like element (in this case row) in order to catch those elements in js with ui.row().classes('bg-gray-800').props(f'width={width} is_nested_drawer=true') as left_drawer:
... To catch "is_nested_drawer" row elements in js document.addEventListener('DOMContentLoaded', () => {
const row_drawers = document.querySelectorAll('.nicegui-row[is_nested_drawer=true]');
const drawer = row_drawers[0] and passed this element as an arg where needed in the remaining js function updateDrawerWidth(parentDrawer, newWidth) {
//const parentDrawer = document.querySelector('.q-drawer');
...
function setupDrawerResize(drawer) {
//const drawer = document.querySelector('.nicegui-row');
... Since a ui.row / column / .... has no toggle, show nor hide functionality, ui.button(icon='menu', on_click=lambda: left_drawer.set_visibility(False if left_drawer.visible else True)) full code: from nicegui import ui
@ui.page('/')
def main_page():
ui.add_head_html('''
<style>
.drawer-resizer {
width: 10px;
height: 100%;
position: absolute;
right: 0;
top: 0;
cursor: col-resize;
z-index: 1000;
background-color: rgba(255, 255, 255, 0.05);
transition: background-color 0.2s ease;
}
.drawer-resizer:hover {
background-color: rgba(255, 255, 255, 0.2);
}
</style>
<script>
function createResizer() {
const resizer = document.createElement('div');
resizer.className = 'drawer-resizer';
return resizer;
}
function updateDrawerWidth(parentDrawer, newWidth) {
//const parentDrawer = document.querySelector('.q-drawer');
if (parentDrawer) {
parentDrawer.style.width = `${newWidth}px`;
// Store the current width in a data attribute
parentDrawer.setAttribute('data-drawer-width', newWidth);
}
}
function setupDrawerResize(drawer) {
//const drawer = document.querySelector('.nicegui-row');
if (!drawer) return;
drawer.style.position = 'relative';
const resizer = createResizer();
drawer.appendChild(resizer);
let isResizing = false;
resizer.onmousedown = (e) => {
isResizing = true;
const startX = e.pageX;
const startWidth = parseInt(getComputedStyle(drawer.parentElement).width);
const onMouseMove = (e) => {
if (!isResizing) return;
const newWidth = startWidth + e.pageX - startX;
updateDrawerWidth(drawer, newWidth);
const page = document.querySelector('.q-page-container');
if (page) {
page.style.paddingLeft = `${newWidth}px`;
}
};
const onMouseUp = () => {
isResizing = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
}
document.addEventListener('DOMContentLoaded', () => {
const row_drawers = document.querySelectorAll('.nicegui-row[drawer=true]');
//const drawer = document.querySelector('.q-drawer');
if (row_drawers.length==0) return;
const drawer = row_drawers[0];
setupDrawerResize(drawer);
// Read the saved width from the data attribute or use default
let drawerWidth = 350; // default width
const savedWidth = drawer.getAttribute('data-drawer-width');
if (savedWidth) {
drawerWidth = parseInt(savedWidth);
} else {
const computedWidth = parseInt(getComputedStyle(drawer).width);
if (computedWidth) {
drawerWidth = computedWidth;
}
}
updateDrawerWidth(drawer, drawerWidth);
const page = document.querySelector('.q-page-container');
if (page) {
// Check if the drawer is currently closed
const isClosed = drawer.classList.contains('q-layout--prevent-focus');
page.style.paddingLeft = isClosed ? '0px' : `${drawerWidth}px`;
}
drawer.addEventListener('transitionend', () => {
const isClosed = drawer.classList.contains('q-layout--prevent-focus');
let drawerWidth = 350; // default width
const savedWidth = drawer.getAttribute('data-drawer-width');
if (savedWidth) {
drawerWidth = parseInt(savedWidth);
} else {
const computedWidth = parseInt(getComputedStyle(drawer).width);
if (computedWidth) {
drawerWidth = computedWidth;
}
}
if (!isClosed) {
updateDrawerWidth(drawer, drawerWidth);
}
const page = document.querySelector('.q-page-container');
if (page) {
page.style.paddingLeft = isClosed ? '0px' : `${drawerWidth}px`;
}
});
});
</script>
''')
with ui.header().classes('w-full flex justify-between items-center p-4 bg-gray-800'):
with ui.row().classes('items-center'):
ui.button(icon='menu', on_click=lambda: left_drawer.set_visibility(False if left_drawer.visible else True))
width = 350
with ui.row():
with ui.row().classes('bg-gray-800').props(f'width={width} is_nested_drawer=true') as left_drawer:
#with ui.left_drawer().props(f'width={width}') as left_drawer:
ui.label("Resizable drawer")
ui.label("Resizable content")
ui.run(dark=True) I get 'odd' results though... This is how far I got. Does anyone has an idea on how to get this working? Thx in advance |
Beta Was this translation helpful? Give feedback.
-
@ljilekor |
Beta Was this translation helpful? Give feedback.
-
I get approx what i need using simple splitters. from nicegui import ui, context
def toggle_splitter(splitter):
if splitter.value:
splitter.prev_value = splitter.value
splitter.set_value(0) # HOW TO ADD TRANSITION ANIM ??
else:
splitter.set_value(splitter.prev_value) # HOW TO ADD TRANSITION ANIM ??
@ui.page('/')
def page():
context.client.content.classes('h-[100vh] gap-0 m-0 p-0')
for x in ['container_1','container_2','container_3']:
with ui.column().classes('w-full h-full gap-0 m-0 p-0 no-wrap border-4 border-green-600').style("min-height:0"):
with ui.row().classes('w-full bg-slate-300 no-wrap border-4 border-purple-600'):
ui.label(x).classes('align-text-bottom')
ui.space().classes('w-full gap-0 m-0 p-0')
but = ui.button(icon='settings').props('dense')
with ui.splitter(value=0).classes('w-full h-full gap-0 m-0 p-0 border-4 border-red-400') as splitter:
with splitter.before:
with ui.column().classes('w-full h-full bg-slate-100 gap-0 m-0 p-0 border-4 border-blue-400'):
ui.label(f'{x} SETTINGS :')
for serie in ['poo','pii','puu','pyy']:
ui.label(serie)
with splitter.after:
with ui.column().classes('w-full h-full gap-0 m-0 p-0 border-4 border-yellow-400'):
for serie in ['zoo','zii','zuu','zyy']:
ui.label(serie)
splitter.prev_value = splitter.value if splitter.value else 20
but.on('click',lambda element=splitter: toggle_splitter(element))
ui.run() but the "animation" part is still missing...
also, a solution using button.on('click', js_handler='...') may be more appropriate as the slide panel could be handled purely on the client-side. Does anyone have an idea on how to improve this please? |
Beta Was this translation helpful? Give feedback.
-
Almost there... from nicegui import ui, context
def toggle_panel_jshandler(element, transition_duration = 0.3):
return f"""
() => {{
const splitter = document.getElementById('c{element.id}');
const beforePanel = splitter.querySelector('.q-splitter__before');
const separator = splitter.querySelector('.q-splitter__separator');
beforePanel.style.transition = 'margin-left {transition_duration}s, opacity {transition_duration}s'
separator.style.transition = 'visibility {transition_duration}s';
if (beforePanel.style.marginLeft === '-'+beforePanel.offsetWidth +'px') {{
beforePanel.style.marginLeft = '0';
beforePanel.style.opacity = '100';
separator.style.visibility = 'visible';
}} else{{
beforePanel.style.marginLeft = '-'+beforePanel.offsetWidth+'px';
beforePanel.style.opacity = '0';
separator.style.visibility = 'hidden';
}}
}}
"""
@ui.page('/')
def page():
context.client.content.classes('h-[100vh] gap-0 m-0 p-0')
for panel_container in ['TEST1','TEST2','TEST3',]:
with ui.column().classes('w-full h-full gap-0 m-0 p-0 border border-black').style("min-height:0"):
with ui.row().classes('w-full m-0 p-0 gap-0 no-wrap').style('background-color: rgb(200,200,200)') as header:
ui.label(panel_container).classes('text-h5')
ui.space().classes('w-full')
toggle_button = ui.button(icon='settings').props('dense')
with ui.row().classes('w-full h-full m-0 p-0 gap-0 no-wrap') as content:
with ui.splitter(value=33, limits=(20,80)).classes('w-full h-full m-0 p-0 gap-0') as splitter:
with splitter.before:
with ui.column().classes('w-full h-full m-0 p-0 gap-0 border-r-2 border-blue-400').style('background-color: rgb(240,240,240)') as left_panel:
ui.label('Panel content')
with splitter.after:
with ui.column().classes('w-full h-full m-0 p-0 gap-0') as right_panel:
ui.label('Content')
toggle_button.on("click", js_handler= toggle_panel_jshandler(splitter, transition_duration=0.5))
# HIDE ON MOUNTED => DOES NOT TRIGGER... ?
splitter.on("mounted", js_handler= f"""
() => {{
console.log('MOUNT c{splitter.id}')
const splitter = document.getElementById('c{splitter.id}');
const beforePanel = splitter.querySelector('.q-splitter__before');
const separator = splitter.querySelector('.q-splitter__separator');
beforePanel.style.marginLeft = '-'+beforePanel.offsetWidth+'px';
beforePanel.style.opacity = '0';
separator.style.visibility = 'hidden';
}}
""")
ui.run() 2 issues remain:
Does anyone have an idea on how to improve and/or finalize this please? |
Beta Was this translation helpful? Give feedback.
-
this works (kinda) fine: from nicegui import ui, context
@ui.page('/')
def page():
context.client.content.classes('h-[100vh] gap-0 m-0 p-0')
ui.add_head_html(f"""
<script>
function togglePanel(elementId, transitionDuration) {{
const splitter = document.getElementById('c' + elementId);
const beforePanel = splitter.querySelector('.q-splitter__before');
const separator = splitter.querySelector('.q-splitter__separator');
beforePanel.style.transition = 'margin-left ' + transitionDuration + 's, opacity ' + transitionDuration + 's';
separator.style.transition = 'visibility ' + transitionDuration + 's';
if (beforePanel.style.display === 'none' || beforePanel.style.display === '') {{
beforePanel.style.display = 'block';
setTimeout(() => {{
beforePanel.style.marginLeft = '0';
beforePanel.style.opacity = '1';
}}, 0);
separator.style.visibility = 'visible';
}} else {{
beforePanel.style.marginLeft = '-' + beforePanel.offsetWidth + 'px';
beforePanel.style.opacity = '0';
separator.style.visibility = 'hidden';
setTimeout(() => {{
beforePanel.style.display = 'none';
}}, transitionDuration * 1000);
}}
}}
function hidePanelOnStartup(elementId) {{
const splitter = document.getElementById('c' + elementId);
const beforePanel = splitter.querySelector('.q-splitter__before');
const separator = splitter.querySelector('.q-splitter__separator');
beforePanel.style.transition = 'none';
separator.style.transition = 'none';
beforePanel.style.marginLeft = '-' + beforePanel.offsetWidth + 'px';
beforePanel.style.opacity = '0';
beforePanel.style.display = 'none';
separator.style.visibility = 'hidden';
}}
</script>
""")
transition_duration = 0.5
for panel_container in ['TEST1', 'TEST2', 'TEST3', ]:
with ui.column().classes('w-full h-full gap-0 m-0 p-0 border border-black').style("min-height:0"):
with ui.row().classes('w-full m-0 p-0 gap-0 no-wrap').style(
'background-color: rgb(200,200,200)') as header:
ui.label(panel_container).classes('text-h5')
ui.space().classes('w-full')
toggle_button = ui.button(icon='settings').props('dense')
with ui.row().classes('w-full h-full m-0 p-0 gap-0 no-wrap') as content:
with ui.splitter(value=33, limits=(20, 80)).classes('w-full h-full m-0 p-0 gap-0') as splitter:
with splitter.before:
with ui.column().classes('w-full h-full m-0 p-0 gap-0 border-r-2 border-blue-400').style(
'background-color: rgb(240,240,240)') as left_panel:
ui.label('Panel content')
with splitter.after:
with ui.column().classes('w-full h-full m-0 p-0 gap-0') as right_panel:
ui.label('Content')
splitter.run_method("hidePanelOnStartup", splitter.id)
toggle_button.on("click", js_handler=f"() => togglePanel('{splitter.id}', {transition_duration})")
ui.run() The only thing is that for some dark reason, Maybe this is something for a separate thread... |
Beta Was this translation helpful? Give feedback.
this works (kinda) fine: