-
Notifications
You must be signed in to change notification settings - Fork 950
add drag and drop based on @btel's work #2720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
985520f
fa5a9d8
e39bc44
711a177
409462f
d3321ff
d97dbe0
a343d15
0fac96c
8ab3a4c
57b64dc
d52e9aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# Copyright (c) Jupyter Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||
|
||
"""Contains the DropWidget class""" | ||
from .widget import Widget, CallbackDispatcher, register, widget_serialization | ||
from .domwidget import DOMWidget | ||
from .widget_core import CoreWidget | ||
import json | ||
from traitlets import Bool, Dict, Unicode, Instance | ||
|
||
|
||
class DropWidget(DOMWidget, CoreWidget): | ||
"""Base widget for the single-child DropBox and DraggableBox widgets""" | ||
|
||
draggable = Bool(default=False).tag(sync=True) | ||
drag_data = Dict().tag(sync=True) | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self._drop_handlers = CallbackDispatcher() | ||
self.on_msg(self._handle_dragdrop_msg) | ||
|
||
def on_drop(self, callback, remove=False): | ||
""" Register a callback to execute when an element is dropped. | ||
|
||
The callback will be called with two arguments, the drop box | ||
widget instance receiving the drop event, and the dropped element data. | ||
|
||
Parameters | ||
---------- | ||
remove: bool (optional) | ||
Set to true to remove the callback from the list of callbacks. | ||
""" | ||
self._drop_handlers.register_callback(callback, remove=remove) | ||
|
||
def drop(self, data): | ||
""" Programmatically trigger a drop event. | ||
This will call the callbacks registered to the drop event. | ||
""" | ||
|
||
if data.get('application/vnd.jupyter.widget-view+json'): | ||
widget_mime = json.loads(data['application/vnd.jupyter.widget-view+json']) | ||
data['widget'] = widget_serialization['from_json']('IPY_MODEL_' + widget_mime['model_id']) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking about this. Why is this method exposed as an API method? If the user is meant to use it from the kernel side, should we not handle the deserialization of the widget reference before calling this? This way, any kernel use could simply pass a widget reference as |
||
|
||
self._drop_handlers(self, data) | ||
|
||
def _handle_dragdrop_msg(self, _, content, buffers): | ||
""" Handle a msg from the front-end. | ||
|
||
Parameters | ||
---------- | ||
content: dict | ||
Content of the msg. | ||
""" | ||
if content.get('event', '') == 'drop': | ||
self.drop(content.get('data', {})) | ||
|
||
@register | ||
class DropBox(DropWidget): | ||
""" A box that receives a drop event | ||
|
||
The DropBox can have one child, and you can attach an `on_drop` handler to it. | ||
|
||
Parameters | ||
---------- | ||
child: Widget instance | ||
The child widget instance that is displayed inside the DropBox | ||
|
||
Examples | ||
-------- | ||
>>> import ipywidgets as widgets | ||
>>> dropbox_widget = widgets.DropBox(Label("Drop something on top of me")) | ||
>>> dropbox_widget.on_drop(lambda box, data: print(data)) | ||
""" | ||
|
||
_model_name = Unicode('DropBoxModel').tag(sync=True) | ||
_view_name = Unicode('DropBoxView').tag(sync=True) | ||
child = Instance(Widget, allow_none=True).tag(sync=True, **widget_serialization) | ||
|
||
def __init__(self, child=None, **kwargs): | ||
super(DropBox, self).__init__(**kwargs, child=child) | ||
|
||
@register | ||
class DraggableBox(DropWidget): | ||
""" A draggable box | ||
|
||
A box widget that can be dragged e.g. on top of a DropBox. The draggable box can | ||
contain a single child, and optionally drag_data which will be received on the widget | ||
it's dropped on. | ||
Draggability can be modified by flipping the boolean ``draggable`` attribute. | ||
|
||
Parameters | ||
---------- | ||
child: Widget instance | ||
The child widget instance that is displayed inside the DropBox | ||
|
||
draggable: Boolean (default True) | ||
Trait that flips whether the draggable box is draggable or not | ||
|
||
drag_data: Dictionary | ||
You can attach custom drag data here, which will be received as an argument on the receiver | ||
side (in the ``on_drop`` event). | ||
|
||
Examples | ||
-------- | ||
>>> import ipywidgets as widgets | ||
>>> draggable_widget = widgets.DraggableBox(Label("You can drag this button")) | ||
>>> draggable_widget.drag_data = {"somerandomkey": "I have this data for you ..."} | ||
""" | ||
|
||
_model_name = Unicode('DraggableBoxModel').tag(sync=True) | ||
_view_name = Unicode('DraggableBoxView').tag(sync=True) | ||
child = Instance(Widget, allow_none=True).tag(sync=True, **widget_serialization) | ||
draggable = Bool(True).tag(sync=True) | ||
drag_data = Dict().tag(sync=True) | ||
|
||
def __init__(self, child=None, **kwargs): | ||
super(DraggableBox, self).__init__(**kwargs, child=child) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,172 @@ | ||||||||||||||||||||
// Copyright (c) Jupyter Development Team. | ||||||||||||||||||||
// Distributed under the terms of the Modified BSD License. | ||||||||||||||||||||
|
||||||||||||||||||||
import { | ||||||||||||||||||||
CoreDOMWidgetModel | ||||||||||||||||||||
} from './widget_core'; | ||||||||||||||||||||
|
||||||||||||||||||||
import { | ||||||||||||||||||||
DOMWidgetView, unpack_models, WidgetModel, WidgetView, JupyterLuminoPanelWidget, reject | ||||||||||||||||||||
} from '@jupyter-widgets/base'; | ||||||||||||||||||||
|
||||||||||||||||||||
import $ from 'jquery'; | ||||||||||||||||||||
|
||||||||||||||||||||
export | ||||||||||||||||||||
class DraggableBoxModel extends CoreDOMWidgetModel { | ||||||||||||||||||||
defaults(): Backbone.ObjectHash { | ||||||||||||||||||||
return { | ||||||||||||||||||||
...super.defaults(), | ||||||||||||||||||||
_view_name: 'DraggableBoxView', | ||||||||||||||||||||
_model_name: 'DraggableBoxModel', | ||||||||||||||||||||
child: null, | ||||||||||||||||||||
draggable: true, | ||||||||||||||||||||
drag_data: {} | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
static serializers = { | ||||||||||||||||||||
...CoreDOMWidgetModel.serializers, | ||||||||||||||||||||
child: {deserialize: unpack_models} | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
export | ||||||||||||||||||||
class DropBoxModel extends CoreDOMWidgetModel { | ||||||||||||||||||||
defaults(): Backbone.ObjectHash { | ||||||||||||||||||||
return { | ||||||||||||||||||||
...super.defaults(), | ||||||||||||||||||||
_view_name: 'DropBoxView', | ||||||||||||||||||||
_model_name: 'DropBoxModel', | ||||||||||||||||||||
child: null | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
static serializers = { | ||||||||||||||||||||
...CoreDOMWidgetModel.serializers, | ||||||||||||||||||||
child: {deserialize: unpack_models} | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
class DragDropBoxViewBase extends DOMWidgetView { | ||||||||||||||||||||
child_view: DOMWidgetView | null; | ||||||||||||||||||||
pWidget: JupyterLuminoPanelWidget; | ||||||||||||||||||||
|
||||||||||||||||||||
_createElement(tagName: string): HTMLElement { | ||||||||||||||||||||
this.pWidget = new JupyterLuminoPanelWidget({ view: this }); | ||||||||||||||||||||
return this.pWidget.node; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
_setElement(el: HTMLElement): void { | ||||||||||||||||||||
if (this.el || el !== this.pWidget.node) { | ||||||||||||||||||||
// Boxes don't allow setting the element beyond the initial creation. | ||||||||||||||||||||
throw new Error('Cannot reset the DOM element.'); | ||||||||||||||||||||
} | ||||||||||||||||||||
this.el = this.pWidget.node; | ||||||||||||||||||||
this.$el = $(this.pWidget.node); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
initialize(parameters: WidgetView.IInitializeParameters): void { | ||||||||||||||||||||
super.initialize(parameters); | ||||||||||||||||||||
this.add_child_model(this.model.get('child')); | ||||||||||||||||||||
this.listenTo(this.model, 'change:child', this.update_child); | ||||||||||||||||||||
|
||||||||||||||||||||
this.pWidget.addClass('jupyter-widgets'); | ||||||||||||||||||||
this.pWidget.addClass('widget-container'); | ||||||||||||||||||||
this.pWidget.addClass('widget-draggable-box'); | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this class needed? It doesn't seem to be used. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I just tried to follow conventions: ipywidgets/packages/controls/src/widget_box.ts Lines 83 to 85 in 7cb20cd
someone who wants to style the drag drop widgets might want to add CSS to this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That could indeed be useful. For the case of the widget_box, there is actually some default css: ipywidgets/packages/controls/css/widgets-base.css Lines 98 to 103 in 7cb20cd
|
||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
add_child_model(model: WidgetModel): Promise<DOMWidgetView> { | ||||||||||||||||||||
return this.create_child_view(model).then((view: DOMWidgetView) => { | ||||||||||||||||||||
if (this.child_view && this.child_view !== null) { | ||||||||||||||||||||
this.child_view.remove(); | ||||||||||||||||||||
} | ||||||||||||||||||||
this.pWidget.addWidget(view.pWidget); | ||||||||||||||||||||
this.child_view = view; | ||||||||||||||||||||
return view; | ||||||||||||||||||||
}).catch(reject('Could not add child view to box', true)); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
update_child(): void { | ||||||||||||||||||||
this.add_child_model(this.model.get('child')); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
remove(): void { | ||||||||||||||||||||
this.child_view = null; | ||||||||||||||||||||
super.remove(); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const JUPYTER_VIEW_MIME = 'application/vnd.jupyter.widget-view+json'; | ||||||||||||||||||||
|
||||||||||||||||||||
export | ||||||||||||||||||||
class DraggableBoxView extends DragDropBoxViewBase { | ||||||||||||||||||||
initialize(parameters: WidgetView.IInitializeParameters): void { | ||||||||||||||||||||
super.initialize(parameters); | ||||||||||||||||||||
this.dragSetup(); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
events(): {[e: string]: string} { | ||||||||||||||||||||
return {'dragstart' : 'on_dragstart'}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
on_dragstart(event: DragEvent): void { | ||||||||||||||||||||
if (event.dataTransfer) { | ||||||||||||||||||||
if (this.model.get('child').get('value')) { | ||||||||||||||||||||
event.dataTransfer?.setData('text/plain', this.model.get('child').get('value')); | ||||||||||||||||||||
} | ||||||||||||||||||||
const drag_data = this.model.get('drag_data'); | ||||||||||||||||||||
for (const datatype in drag_data) { | ||||||||||||||||||||
event.dataTransfer.setData(datatype, drag_data[datatype]); | ||||||||||||||||||||
} | ||||||||||||||||||||
event.dataTransfer.setData(JUPYTER_VIEW_MIME, JSON.stringify({ | ||||||||||||||||||||
"model_id": this.model.model_id, | ||||||||||||||||||||
"version_major": 2, | ||||||||||||||||||||
"version_minor": 0 | ||||||||||||||||||||
})); | ||||||||||||||||||||
event.dataTransfer.dropEffect = 'copy'; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
dragSetup(): void { | ||||||||||||||||||||
this.el.draggable = this.model.get('draggable'); | ||||||||||||||||||||
this.model.on('change:draggable', this.on_change_draggable, this); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
on_change_draggable(): void { | ||||||||||||||||||||
this.el.draggable = this.model.get('draggable'); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
export | ||||||||||||||||||||
class DropBoxView extends DragDropBoxViewBase { | ||||||||||||||||||||
events(): {[e: string]: string} { | ||||||||||||||||||||
return { | ||||||||||||||||||||
'drop': '_handle_drop', | ||||||||||||||||||||
'dragover': 'on_dragover' | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
_handle_drop(event: DragEvent): void { | ||||||||||||||||||||
event.preventDefault(); | ||||||||||||||||||||
|
||||||||||||||||||||
const datamap: {[e: string]: string} = {}; | ||||||||||||||||||||
|
||||||||||||||||||||
if (event.dataTransfer) | ||||||||||||||||||||
{ | ||||||||||||||||||||
for (let i=0; i < event.dataTransfer.types.length; i++) { | ||||||||||||||||||||
const t = event.dataTransfer.types[i]; | ||||||||||||||||||||
datamap[t] = event.dataTransfer?.getData(t); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
this.send({event: 'drop', data: datamap}); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
on_dragover(event: DragEvent): void { | ||||||||||||||||||||
event.preventDefault(); | ||||||||||||||||||||
event.stopPropagation(); | ||||||||||||||||||||
if (event.dataTransfer) { | ||||||||||||||||||||
event.dataTransfer.dropEffect = 'copy'; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} |
Uh oh!
There was an error while loading. Please reload this page.