Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # 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