Skip to content
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

add drag and drop based on @btel's and @wolfv's work #3803

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
726 changes: 726 additions & 0 deletions docs/source/examples/Drag and Drop.ipynb

Large diffs are not rendered by default.

74 changes: 71 additions & 3 deletions docs/source/examples/Widget Events.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
{
"cell_type": "markdown",
"metadata": {
"tags": ["remove-cell"]
"tags": [
"remove-cell"
]
},
"source": [
"[Index](Index.ipynb) - [Back](Output%20Widget.ipynb) - [Next](Widget%20Styling.ipynb)"
Expand Down Expand Up @@ -38,7 +40,9 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": ["remove-cell"]
"tags": [
"remove-cell"
]
},
"outputs": [],
"source": [
Expand Down Expand Up @@ -513,10 +517,74 @@
"widgets.VBox([slider, text])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Drag and Drop\n",
"\n",
"Widgets can support drag-drop interactions by wrapping them in DraggableBox and DropBox.\n",
"\n",
"A on_drop callback can be added to the DropBox to handle the event of receiving a drop action.\n",
"\n",
"The DraggableBox widget automatically passes the value as text and widget to the DropBox. Additional dict data can be added to DraggableBox with the drag_data trait."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def DraggableLabel(value, draggable=True):\n",
" box = widgets.DraggableBox(widgets.Label(value))\n",
" box.draggable = draggable\n",
" return box\n",
"\n",
"label = DraggableLabel(\"Drag me\", draggable=True)\n",
"label.drag_data = {'application/custom-data' : 'Custom data'}\n",
"label"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def on_drop_handler(widget, data):\n",
" \"\"\"\"Arguments:\n",
" \n",
" widget : widget class\n",
" widget on which something was dropped\n",
" \n",
" data : dict\n",
" extra data sent from the dragged widget\"\"\"\n",
" \n",
" text = data['text/plain']\n",
" widget_id = data['widget'].model_id\n",
" custom_data = data['application/custom-data']\n",
" value = \"you dropped widget ID '{}...' with text '{}' and custom data '{}'\".format(widget_id[:5], text, custom_data)\n",
" widget.child.value = value\n",
"\n",
"box = widgets.DropBox(widgets.Label(\"Drop here\"))\n",
"box.on_drop(on_drop_handler)\n",
"box"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"More examples of using drag drop are available in - [Drag and Drop](Drag%20and%20Drop.ipynb)"
]
},
{
"cell_type": "markdown",
"metadata": {
"tags": ["remove-cell"]
"tags": [
"remove-cell"
]
},
"source": [
"[Index](Index.ipynb) - [Back](Output%20Widget.ipynb) - [Next](Widget%20Styling.ipynb)"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/controls/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export * from './widget_tagsinput';
export * from './widget_string';
export * from './widget_description';
export * from './widget_upload';
export * from './widget_dragdrop';

export const version = (require('../package.json') as any).version;
178 changes: 178 additions & 0 deletions packages/controls/src/widget_dragdrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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;
luminoWidget: JupyterLuminoPanelWidget;

_createElement(tagName: string): HTMLElement {
this.luminoWidget = new JupyterLuminoPanelWidget({ view: this });
return this.luminoWidget.node;
}

_setElement(el: HTMLElement): void {
if (this.el || el !== this.luminoWidget.node) {
// Boxes don't allow setting the element beyond the initial creation.
throw new Error('Cannot reset the DOM element.');
}
this.el = this.luminoWidget.node;
this.$el = $(this.luminoWidget.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.luminoWidget.addClass('jupyter-widgets');
this.luminoWidget.addClass('widget-container');
this.luminoWidget.addClass('widget-draggable-box');
}

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.luminoWidget.addWidget(view.luminoWidget);
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';
}
}
}
Loading