diff --git a/docs/source/examples/Drag and Drop.ipynb b/docs/source/examples/Drag and Drop.ipynb new file mode 100644 index 0000000000..1134d3d971 --- /dev/null +++ b/docs/source/examples/Drag and Drop.ipynb @@ -0,0 +1,726 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Drag and Drop\n", + "\n", + "In this notebook we introduce the `DraggableBox` and `DropBox` widgets, that can be used to drag and drop widgets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Draggable Box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`DraggableBox` is a widget that wraps other widgets and makes them draggable.\n", + "\n", + "For example we can build a custom `DraggableLabel` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import Label, DraggableBox, Textarea" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def DraggableLabel(value, draggable=True):\n", + " box = DraggableBox(Label(value))\n", + " box.draggable = draggable\n", + " return box" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "DraggableLabel(\"Hello Drag\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can drag this label anywhere (could be your shell, etc.), but also to a text area:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Textarea()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Drop Box\n", + "\n", + "`DropBox` is a widget that can receive other `DraggableBox` widgets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import DropBox\n", + "\n", + "\n", + "box = DropBox(Label(\"Drop on me\"))\n", + "box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`DropBox` can become the drop zone (you can drop other stuff on it) by implementing the `on_drop` handler:" + ] + }, + { + "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", + " text = data['text/plain']\n", + " widget.child.value = \"congrats, you dropped '{}'\".format(text)\n", + "\n", + "box.on_drop(on_drop_handler)\n", + "box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, drag this label on the box above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label = DraggableLabel(\"Drag me\")\n", + "label" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note** : You can also drop other stuff (like files, images, links, selections, etc). Try it out!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Changing any widget into a drop zone with arbitrary drop operations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you have more specific needs for the drop behavior you can implement them in the DropBox `on_drop` handler." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This DropBox creates new `Button` widgets using the text data of the `DraggableLabel` widget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import Button, Layout\n", + "\n", + "label = DraggableLabel(\"Drag me\", draggable=True)\n", + "label" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "or **Select and drag me!**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def on_drop(widget, data):\n", + " text = data['text/plain']\n", + " widget.child = Button(description=text.upper())\n", + "\n", + "box = DropBox(Label(\"Drop here!\"), layout=Layout(width='200px', height='100px'))\n", + "box.on_drop(on_drop)\n", + "box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding widgets to a container with a handler" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also reproduce the Box example (adding elements to a `Box` container) using a `DropBox` with a custom handler:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label = DraggableLabel(\"Drag me\", draggable=True)\n", + "label" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import VBox\n", + "\n", + "def on_drop(widget, data):\n", + " source = data['widget']\n", + " widget.child.children += (source, )\n", + "\n", + "box = DropBox(VBox([Label('Drop here')]), layout=Layout(width='200px', height='100px'))\n", + "box.on_drop(on_drop)\n", + "box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation**: The `Label` widget sets data on the drop event of type `application/x-widget` that contains the widget id of the dragged widget." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting custom data on the drop event" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also set custom data on a `DraggableBox` widget that can be retrieved and used in `on_drop` event." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "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 = DropBox(Label(\"Drop here\"))\n", + "box.on_drop(on_drop_handler)\n", + "box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Making any widget draggable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`DraggableBox` can be used to wrap any widget so that it can be dragged and dropped. For example sliders can also be dragged:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import DraggableBox, Button, VBox, Layout, IntSlider, Label" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "slider = IntSlider(layout=Layout(width='250px'), description='Drag me')\n", + "DraggableBox(slider, draggable=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def attach_to_box(box, widget):\n", + " box.children += (widget, )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vdropbox = DropBox(VBox([Label('Drop sliders below')], layout=Layout(width='350px', height='150px')))\n", + "vdropbox.on_drop(lambda *args: attach_to_box(vdropbox.child, args[1]['widget']))\n", + "vdropbox" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Draggable data columns" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This implements a more complex example.\n", + "\n", + "**Note**: You need to have bqplot installed for this example to work." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import bqplot.pyplot as plt\n", + "from ipywidgets import Label, GridspecLayout, DropBox, Layout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def create_table(data):\n", + " n_cols = len(data)\n", + " n_rows = max(len(column) for column in data.values())\n", + " grid = GridspecLayout(n_rows+1, n_cols)\n", + " columnames = list(data.keys())\n", + " for i in range(n_cols):\n", + " column = columnames[i]\n", + " data_json = json.dumps(data[column])\n", + " grid[0, i] = DraggableLabel(columnames[i], draggable=True)\n", + " grid[0, i].draggable = True\n", + " grid[0, i].drag_data = {'data/app' : data_json}\n", + " for j in range(n_rows):\n", + " grid[j+1, i] = DraggableLabel(str(data[column][j]), draggable=True)\n", + " grid[j+1, i].drag_data = {'data/app' : data_json}\n", + " return grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def box_ondrop(widget, data):\n", + " fig = plt.figure()\n", + " y = json.loads(data['data/app'])\n", + " plt.plot(y)\n", + " widget.child = fig\n", + " \n", + "box = DropBox(Label(\"Drag data from the table and drop it here.\"), layout=Layout(height='500px', width='800px'))\n", + "box.on_drop(box_ondrop)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_data = {\n", + " 'col1': [ 4, 2, 1],\n", + " 'col2': [ 1, 3, 4],\n", + " 'col3': [-1, 2, -3]\n", + "}\n", + "create_table(plot_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can drag the data by the labels or values (the whole column will be dragged)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot builder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is another example allowing to build plots from a list of labels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import bqplot as bq\n", + "from ipywidgets import SelectMultiple, Layout, DraggableBox, DropBox, HBox\n", + "\n", + "select_list = SelectMultiple(\n", + " options=['Apples', 'Oranges', 'Pears'],\n", + " value=['Oranges'],\n", + " description='Fruits',\n", + " disabled=False\n", + ")\n", + "select_box = DraggableBox(select_list, draggable=True)\n", + "\n", + "fruits = {\n", + " 'Apples' : 5,\n", + " 'Oranges' : 1,\n", + " 'Pears': 3\n", + "}\n", + "\n", + "fig = bq.Figure(marks=[], fig_margin = dict(left=50, right=0, top=0, bottom=70))\n", + "fig.layout.height='300px'\n", + "fig.layout.width='300px'\n", + "fig_box = DropBox(fig)\n", + "\n", + "fig2 = bq.Figure(marks=[], fig_margin = dict(left=50, right=0, top=0, bottom=70))\n", + "fig2.layout.height='300px'\n", + "fig2.layout.width='300px'\n", + "fig2_box = DropBox(fig2)\n", + "\n", + "def on_fig_drop(widget, data):\n", + " \n", + " # get the figure widget\n", + " fig = widget.child\n", + " #get the selection widget\n", + " selection_widget = data['widget'].child\n", + " \n", + " # get the selected fruits and prices\n", + " selected = selection_widget.value\n", + " prices = [fruits[f] for f in selected]\n", + " \n", + " sc_x = bq.OrdinalScale()\n", + " sc_y = bq.LinearScale()\n", + "\n", + " ax_x = bq.Axis(scale=sc_x)\n", + " ax_y = bq.Axis(scale=sc_y, orientation='vertical')\n", + " bars = bq.Bars(x=selected, y=prices, scales={'x' : sc_x, 'y' : sc_y })\n", + " fig.axes = [ax_x, ax_y]\n", + " fig.marks = [bars]\n", + "\n", + "fig_box.on_drop(on_fig_drop)\n", + "fig2_box.on_drop(on_fig_drop)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select and drag fruits from the list to the boxes on the right. It's better to drag the selection using the \"Fruits\" label, due to a smalll glitch in the selection widget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "HBox([select_box,\n", + " fig_box,\n", + " fig2_box])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dashboard Builder\n", + "\n", + "The drag and drop widgets can also be used to build a dashboard interactively.\n", + "\n", + "First let's define the structure of the dashboard by using the `AppLayout` layout template widget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import AppLayout, Button, DropBox, Layout, VBox" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Whenever a `DraggableBox` widget is dropped in a `Dropbox`, the content of the `Dropbox` will be replaced by the widget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def attach_to_box(box, widget):\n", + " box.child = widget\n", + "\n", + "\n", + "def create_expanded_dropbox(button_style):\n", + " box = DropBox(Button(description='Drop widget here', button_style=button_style, layout=Layout(width='100%', height='100%')))\n", + " box.on_drop(lambda *args: attach_to_box(box, args[1]['widget']))\n", + " return box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create the app layout:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "header = create_expanded_dropbox('success')\n", + "left_sidebar = create_expanded_dropbox('info')\n", + "center = create_expanded_dropbox('warning')\n", + "right_sidebar = create_expanded_dropbox('info')\n", + "footer = create_expanded_dropbox('success')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app = AppLayout(\n", + " header=header,\n", + " left_sidebar=left_sidebar,\n", + " center=center,\n", + " right_sidebar=right_sidebar,\n", + " footer=footer\n", + ")\n", + "app" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's create the widgets that will be part of the dashboard." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import DraggableBox, Dropdown, IntProgress, IntSlider, Label, Tab, Text, link" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "title = Label('My Custom Dashboard', layout=Layout(display='flex', justify_content='center', width='auto'))\n", + "DraggableBox(title, draggable=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "slider = IntSlider(min=0, max=10, step=1, layout=Layout(width='auto'), description='Slider')\n", + "DraggableBox(slider, draggable=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "progress = IntProgress(\n", + " min=0,\n", + " max=10,\n", + " step=1,\n", + " description='Loading:',\n", + " orientation='horizontal',\n", + " layout=Layout(width='auto')\n", + ")\n", + "link((slider, 'value'), (progress, 'value'))\n", + "DraggableBox(progress, draggable=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tab_contents = ['P0', 'P1', 'P2', 'P3', 'P4']\n", + "children = [Text(description=name) for name in tab_contents]\n", + "tab = Tab()\n", + "tab.children = children\n", + "tab.titles = [str(i) for i in range(len(children))]\n", + "\n", + "DraggableBox(tab, draggable=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's drag the widgets and drop them in the app layout!\n", + "\n", + "In JupyterLab you can open the widget in a different panel by right clicking on the `AppLayout` widget and selecting `Create New View for Output`:\n", + "\n", + "![create-new-view-for-output](./images/create-new-view-for-output.png)\n", + "\n", + "This makes dragging widgets to the app layout more convenient." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/examples/images/create-new-view-for-output.png b/docs/source/examples/images/create-new-view-for-output.png new file mode 100644 index 0000000000..cac58459c5 Binary files /dev/null and b/docs/source/examples/images/create-new-view-for-output.png differ diff --git a/ipywidgets/widgets/__init__.py b/ipywidgets/widgets/__init__.py index a7b5687b45..d5abb0391c 100644 --- a/ipywidgets/widgets/__init__.py +++ b/ipywidgets/widgets/__init__.py @@ -27,3 +27,4 @@ from .widget_style import Style from .widget_templates import TwoByTwoLayout, AppLayout, GridspecLayout from .widget_upload import FileUpload +from .widget_dragdrop import DraggableBox, DropBox diff --git a/ipywidgets/widgets/widget.py b/ipywidgets/widgets/widget.py index f185c22fc6..cd3556c043 100644 --- a/ipywidgets/widgets/widget.py +++ b/ipywidgets/widgets/widget.py @@ -20,7 +20,7 @@ from .._version import __protocol_version__, __jupyter_widgets_base_version__ PROTOCOL_VERSION_MAJOR = __protocol_version__.split('.')[0] -def _widget_to_json(x, obj): +def _widget_to_json(x, obj=None): if isinstance(x, dict): return {k: _widget_to_json(v, obj) for k, v in x.items()} elif isinstance(x, (list, tuple)): @@ -30,7 +30,7 @@ def _widget_to_json(x, obj): else: return x -def _json_to_widget(x, obj): +def _json_to_widget(x, obj=None): if isinstance(x, dict): return {k: _json_to_widget(v, obj) for k, v in x.items()} elif isinstance(x, (list, tuple)): diff --git a/ipywidgets/widgets/widget_dragdrop.py b/ipywidgets/widgets/widget_dragdrop.py new file mode 100644 index 0000000000..01e6c200c0 --- /dev/null +++ b/ipywidgets/widgets/widget_dragdrop.py @@ -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']) + + 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) diff --git a/packages/controls/src/index.ts b/packages/controls/src/index.ts index 56e455553c..aba5b68537 100644 --- a/packages/controls/src/index.ts +++ b/packages/controls/src/index.ts @@ -20,5 +20,6 @@ export * from './widget_selectioncontainer'; 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; diff --git a/packages/controls/src/widget_dragdrop.ts b/packages/controls/src/widget_dragdrop.ts new file mode 100644 index 0000000000..5404864b90 --- /dev/null +++ b/packages/controls/src/widget_dragdrop.ts @@ -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'); + } + + add_child_model(model: WidgetModel): Promise { + 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'; + } + } +} diff --git a/packages/schema/jupyterwidgetmodels.latest.json b/packages/schema/jupyterwidgetmodels.latest.json index 7c37698aa8..7aa4e42c27 100644 --- a/packages/schema/jupyterwidgetmodels.latest.json +++ b/packages/schema/jupyterwidgetmodels.latest.json @@ -2122,6 +2122,206 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "DraggableBoxModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "DraggableBoxView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "allow_none": true, + "default": "reference to new instance", + "help": "", + "name": "child", + "type": "reference", + "widget": "Widget" + }, + { + "default": {}, + "help": "", + "name": "drag_data", + "type": "object" + }, + { + "default": true, + "help": "", + "name": "draggable", + "type": "bool" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "DraggableBoxModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "DraggableBoxView", + "version": "2.0.0" + } + }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "DropBoxModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "DropBoxView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "allow_none": true, + "default": "reference to new instance", + "help": "", + "name": "child", + "type": "reference", + "widget": "Widget" + }, + { + "default": {}, + "help": "", + "name": "drag_data", + "type": "object" + }, + { + "default": false, + "help": "", + "name": "draggable", + "type": "bool" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "DropBoxModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "DropBoxView", + "version": "2.0.0" + } + }, { "attributes": [ { diff --git a/packages/schema/jupyterwidgetmodels.latest.md b/packages/schema/jupyterwidgetmodels.latest.md index 3386fde06e..8540d05cdd 100644 --- a/packages/schema/jupyterwidgetmodels.latest.md +++ b/packages/schema/jupyterwidgetmodels.latest.md @@ -376,6 +376,42 @@ Attribute | Type | Default | Help `source` | array | `[]` | The source (widget, 'trait_name') pair `target` | array | `[]` | The target (widget, 'trait_name') pair +### DraggableBoxModel (@jupyter-widgets/controls, 2.0.0); DraggableBoxView (@jupyter-widgets/controls, 2.0.0) + +Attribute | Type | Default | Help +-----------------|------------------|------------------|---- +`_dom_classes` | array of string | `[]` | CSS classes applied to widget DOM element +`_model_module` | string | `'@jupyter-widgets/controls'` | +`_model_module_version` | string | `'2.0.0'` | +`_model_name` | string | `'DraggableBoxModel'` | +`_view_module` | string | `'@jupyter-widgets/controls'` | +`_view_module_version` | string | `'2.0.0'` | +`_view_name` | string | `'DraggableBoxView'` | +`child` | `null` or reference to Widget widget | reference to new instance | +`drag_data` | object | `{}` | +`draggable` | boolean | `true` | +`layout` | reference to Layout widget | reference to new instance | +`tabbable` | `null` or boolean | `null` | Is widget tabbable? +`tooltip` | `null` or string | `null` | A tooltip caption. + +### DropBoxModel (@jupyter-widgets/controls, 2.0.0); DropBoxView (@jupyter-widgets/controls, 2.0.0) + +Attribute | Type | Default | Help +-----------------|------------------|------------------|---- +`_dom_classes` | array of string | `[]` | CSS classes applied to widget DOM element +`_model_module` | string | `'@jupyter-widgets/controls'` | +`_model_module_version` | string | `'2.0.0'` | +`_model_name` | string | `'DropBoxModel'` | +`_view_module` | string | `'@jupyter-widgets/controls'` | +`_view_module_version` | string | `'2.0.0'` | +`_view_name` | string | `'DropBoxView'` | +`child` | `null` or reference to Widget widget | reference to new instance | +`drag_data` | object | `{}` | +`draggable` | boolean | `false` | +`layout` | reference to Layout widget | reference to new instance | +`tabbable` | `null` or boolean | `null` | Is widget tabbable? +`tooltip` | `null` or string | `null` | A tooltip caption. + ### DropdownModel (@jupyter-widgets/controls, 2.0.0); DropdownView (@jupyter-widgets/controls, 2.0.0) Attribute | Type | Default | Help