TBR reader/web: adds UI for drag and drop device-set management.
Adds event handling from initial work done by @jwnichls so that drag and drop
interactions are possible on lists of devices presented via a modal overlay.
There is still some work left which is documented in #38.
Change-Id: I347195ba1447913382e013e99f9bf4cecadc12aa
diff --git a/web/Makefile b/web/Makefile
index e32d7f8..18f1b35 100644
--- a/web/Makefile
+++ b/web/Makefile
@@ -125,12 +125,10 @@
--v23.credentials="credentials" \
--v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'
-.PHONY:
vdl: browser/vanadium/vdl/index.js
- @true # silence "make: Nothing to be done for `vdl'."
browser/vanadium/vdl/index.js: $(vdl_files)
VDLPATH=$(VDLPATH) vdl generate --lang javascript \
-js-out-dir ./browser/vanadium \
vdl
-
+ @touch $@
diff --git a/web/browser/components/device-set/index.js b/web/browser/components/device-set/index.js
index 5b60ea5..bc1ad58 100644
--- a/web/browser/components/device-set/index.js
+++ b/web/browser/components/device-set/index.js
@@ -7,7 +7,9 @@
var extend = require('xtend');
var file = require('../file');
var hg = require('mercury');
+var modal = require('../modal');
var read = require('../../dom/read-blob');
+var util = require('../../util');
var uuid = require('uuid').v4;
module.exports = {
@@ -30,6 +32,8 @@
var atom = hg.state({
id: hg.value(options.id),
error: hg.value(null),
+ modal: modal.state(options.modal),
+
file: file.state(options.file),
pdf: hg.value(null),
pages: hg.varhash({
@@ -37,12 +41,27 @@
current: options.pages.current || 1,
}),
progress: hg.value(0),
+
devices: hg.varhash(options.devices, device.state),
+
+ manager: hg.struct({
+ active: hg.value(true),
+ dragID: hg.value(''),
+ overID: hg.value(''),
+ }),
channels: {
load: load,
previous: previous,
next: next,
- manage: manage
+
+ manage: manage,
+
+ unlink: unlink,
+ link: link,
+
+ drag: drag,
+ reorder: reorder,
+ reset: reset
}
});
@@ -130,10 +149,85 @@
}
function manage(state, data) {
- debug('manage device set: %s', state.id());
+ state.modal.active.set(true);
+}
+
+function unlink(state, data) {
+ var device = state.devices.get(data.id);
+ device.linked.set(false);
+ device.index.set(null);
+
+ // TODO(jasoncampbell): Refactor so that re-indexing is not required.
+ util
+ .toArray(state.devices)
+ .sort(function sortByIndex(a, b) {
+ var value = 0;
+
+ if (a.index > b.index) {
+ value = 1;
+ }
+
+ if (a.index < b.index) {
+ value = -1;
+ }
+
+ return value;
+ }).filter(function filter(device) {
+ return device.linked();
+ }).forEach(function (device, index) {
+ debug('reindexing: %s', device.id());
+ device.index.set(index);
+ });
+
+ // Reset the manager.
+ reset(state, data);
+}
+
+function link(state, data) {
+ var device = state.devices.get(data.id);
+
+ // This could go away once the drag handlers broadcast correctly.
+ if (! data.id || !device || device.linked()) {
+ return;
+ }
+
+ // TODO(jasoncampbell): Refactor so that re-indexing is not required.
+ var linked = util.toArray(state.devices()).filter(function(device) {
+ return device.linked;
+ });
+
+ device.linked.set(true);
+ device.index.set(linked.length);
}
// Prevent circular references when serializing state.
function _PDFDocumentProxyToJSON() {
return {};
}
+
+function drag(state, data) {
+ if (data.dragging) {
+ state.manager.dragID.set(data.id);
+ } else {
+ state.manager.dragID.set('');
+ }
+}
+
+function reorder(state, data) {
+ if (!data.dragging) {
+ return;
+ }
+
+ var droptarget = state.devices.get(data.droptarget);
+ var dragtarget = state.devices.get(data.dragtarget);
+ var index = droptarget.index();
+
+ // Swap drag and drop target indexes.
+ droptarget.index.set(dragtarget.index());
+ dragtarget.index.set(index);
+}
+
+function reset(state, data) {
+ state.manager.overID.set('');
+ state.manager.dragID.set('');
+}
diff --git a/web/browser/components/device-set/manager.css b/web/browser/components/device-set/manager.css
new file mode 100644
index 0000000..63b6b98
--- /dev/null
+++ b/web/browser/components/device-set/manager.css
@@ -0,0 +1,31 @@
+/* Copyright 2015 The Vanadium Authors. All rights reserved. */
+/* Use of this source code is governed by a BSD-style */
+/* license that can be found in the LICENSE file. */
+
+@import "../base/variables.css";
+@import "../base/typography.css";
+
+.manager .devices-linked,
+.manager .devices-unlinked {
+ margin-bottom: var(--gutter);
+}
+
+.manager .devices-unlinked {
+ border: 1px solid yellow;
+}
+
+.manager .title {
+ inherits: .type-title;
+ margin-bottom: var(--gutter);
+}
+
+.manager .list {
+ border: 1px solid red;
+}
+
+.manager .item {
+ margin: var(--gutter);
+ padding: var(--gutter);
+ cursor: move;
+ border: 1px solid magenta;
+}
diff --git a/web/browser/components/device-set/pdf-viewer.css b/web/browser/components/device-set/pdf-viewer.css
index 662cb45..4e20723 100644
--- a/web/browser/components/device-set/pdf-viewer.css
+++ b/web/browser/components/device-set/pdf-viewer.css
@@ -41,7 +41,7 @@
align-items: center;
background-color: var(--white);
box-shadow: var(--drop-shadow);
- z-index: 900;
+ z-index: 4;
display: flex;
}
diff --git a/web/browser/components/device-set/render.js b/web/browser/components/device-set/render.js
index 296ae3b..1ed0566 100644
--- a/web/browser/components/device-set/render.js
+++ b/web/browser/components/device-set/render.js
@@ -5,27 +5,35 @@
var click = require('../../events/click');
var css = require('./pdf-viewer.css');
var debug = require('debug')('reader:device-set');
+var drag = require('../../events/drag');
+var dragover = require('../../events/dragover');
+var drop = require('../../events/drop');
var format = require('format');
var h = require('mercury').h;
var hg = require('mercury');
var insert = require('insert-css');
+var managerCSS = require('./manager.css');
+var modal = require('../modal');
var PDFWidget = require('./pdf-widget');
module.exports = render;
function render(state, channels) {
+ debug('render: %o', state);
+
insert(css);
+ var node = manager(state, channels);
+
return h('.pdf-viewer', [
hg.partial(progress, state.progress),
hg.partial(controls, state, channels),
- h('.pdf-widget', new PDFWidget(state))
+ h('.pdf-widget', new PDFWidget(state)),
+ hg.partial(modal.render, state.modal, node)
]);
}
function progress(state) {
- debug('progress: %s', state);
-
if (state >= 100) {
return h('.progress.hidden');
}
@@ -76,3 +84,104 @@
]);
}
+function manager(state, channels) {
+ // If the initiator is the current device show the device list manager UI.
+ // Else show the remote control manager.
+ insert(managerCSS);
+
+ // TODO(jasoncampbell): Refactor so that re-indexing is not required.
+ // Use a single loop to create each linked, and unlinked vnode children arrays
+ // which are sorted in the correct order.
+ var linked = [];
+ var unlinked = [];
+ var keys = Object.keys(state.devices);
+ var length = keys.length;
+ for (var i = 0; i < length; i++) {
+ var id = keys[i];
+ var device = state.devices[id];
+ if (device.linked) {
+ // Add the node to the linked array at the correct index to preserve the
+ // order.
+ linked.push(device);
+ } else {
+ unlinked.push(device);
+ }
+ }
+
+ linked = linked.sort(function sort(a, b){
+ var value = 0;
+
+ if (a.index > b.index) {
+ value = 1;
+ }
+
+ if (a.index < b.index) {
+ value = -1;
+ }
+
+ return value;
+ });
+
+ return h('.manager', [
+ h('.devices-linked', {
+ 'ev-event': [
+ dragover(channels.link, {
+ id: state.manager.dragID
+ }),
+ drop(channels.reset)
+ ]
+ }, [
+ h('.title', 'Linked devices (drag to reorder)'),
+ h('.list', linked.map(function (device) {
+ return hg.partial(item, device, state, channels);
+ }))
+ ]),
+ h('.devices-unlinked', {
+ 'ev-event': drop(channels.unlink, {
+ id: state.manager.dragID
+ })
+ }, [
+ h('.title', 'Unlinked devices (drag here to unlink)'),
+ h('.list', unlinked.map(function (device) {
+ return hg.partial(item, device, state, channels);
+ }))
+ ])
+ ]);
+}
+
+function item(device, state, channels) {
+ if (state.manager.dragID === device.id) {
+ return h('.item.placeholder', {
+ 'ev-event': drop(channels.reset)
+ }, format('PLACEHOLDER: %s - index: %s', device.id, device.index));
+ }
+
+ var current = device.current ? 'CURRENT' : '';
+
+ var classNames = [];
+ if (state.manager.overID) {
+ classNames.push('over');
+ }
+
+ if (device.current) {
+ classNames.push('current');
+ }
+
+ var events = [
+ drag(channels.drag, { id: device.id }),
+ ];
+
+ if (device.linked) {
+ // Only reorder linked devices.
+ events.push(dragover(channels.reorder, {
+ droptarget: device.id,
+ dragtarget: state.manager.dragID
+ }));
+ }
+
+ return h('.item', {
+ className: classNames.join(' '),
+ draggable: true,
+ 'ev-event': events,
+ }, format('%s - index: %s %s', device.id, device.index, current));
+}
diff --git a/web/browser/components/device-sets/index.js b/web/browser/components/device-sets/index.js
index 5867b40..515e271 100644
--- a/web/browser/components/device-sets/index.js
+++ b/web/browser/components/device-sets/index.js
@@ -63,6 +63,13 @@
ds.devices.put(d.id(), d);
state.collection.put(ds.id(), ds);
+
+ // TODO(jasoncampbell): Remove once syncbase is hooked back up.
+ debug('adding 3 more fake devices for debugging');
+ for (var i = 0; i < 3; i++) {
+ d = device.state({ index: i + 1 });
+ ds.devices.put(d.id(), d);
+ }
}
function remove(state, data) {
diff --git a/web/browser/components/device/index.js b/web/browser/components/device/index.js
index 0d3b036..aee0031 100644
--- a/web/browser/components/device/index.js
+++ b/web/browser/components/device/index.js
@@ -17,12 +17,17 @@
function state(options, key) {
options = extend({
id: key || uuid(),
- screen: {}
+ linked: true,
+ screen: {},
+ index: 0
}, options);
+
debug('init: %o', options);
var atom = hg.state({
id: hg.value(options.id),
+ linked: hg.value(options.linked),
+ index: hg.value(options.index),
current: hg.value(options.current || false),
type: hg.value(options.type),
alias: hg.value(options.alias),
diff --git a/web/browser/components/header/index.css b/web/browser/components/header/index.css
index 9a1ca7f..aa45eb8 100644
--- a/web/browser/components/header/index.css
+++ b/web/browser/components/header/index.css
@@ -14,7 +14,7 @@
background-color: var(--cyan-800);
box-shadow: var(--drop-shadow);
color: var(--white);
- z-index: 900;
+ z-index: 2;
}
header.hidden {
diff --git a/web/browser/components/modal/index.js b/web/browser/components/modal/index.js
new file mode 100644
index 0000000..92112f2
--- /dev/null
+++ b/web/browser/components/modal/index.js
@@ -0,0 +1,56 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var assert = require('assert');
+var click = require('../../events/click');
+var css = require('./modal.css');
+var extend = require('xtend');
+var h = require('mercury').h;
+var hg = require('mercury');
+var insert = require('insert-css');
+
+module.exports = {
+ state: state,
+ render: render
+};
+
+var defaults = {
+ active: false
+};
+
+function state(options) {
+ options = extend(defaults, options);
+
+ return hg.state({
+ active: hg.value(!!options.active),
+ channels: {
+ show: show,
+ hide: hide
+ }
+ });
+}
+
+function show(state, data) {
+ state.active.set(true);
+}
+
+function hide(state, data) {
+ state.active.set(false);
+}
+
+function render(state, vnode) {
+ assert.equal(vnode.constructor.name,
+ 'VirtualNode',
+ 'The second argument must be a VirtualNode.');
+ insert(css);
+
+ return h('.modal', {
+ className: state.active ? 'active' : 'hidden'
+ }, [
+ h('.modal-blocker', {
+ 'ev-click': click(state.channels.hide)
+ }),
+ h('.modal-dialog', vnode)
+ ]);
+}
\ No newline at end of file
diff --git a/web/browser/components/modal/modal.css b/web/browser/components/modal/modal.css
new file mode 100644
index 0000000..78190db
--- /dev/null
+++ b/web/browser/components/modal/modal.css
@@ -0,0 +1,41 @@
+/* Copyright 2015 The Vanadium Authors. All rights reserved. */
+/* Use of this source code is governed by a BSD-style */
+/* license that can be found in the LICENSE file. */
+
+@import "../base/variables.css";
+
+.modal {
+ visibility: visible;
+}
+
+.modal.hidden {
+ visibility: hidden;
+}
+
+.modal-blocker {
+ position: fixed;
+ z-index: 888;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.24);
+}
+
+.modal-dialog {
+ position: absolute;
+ z-index: 999;
+ /* HACK: Header's title line-height + 2x the gutters. */
+ top: calc(20px + (var(--gutter) * 3));
+ right: 0;
+ bottom: 0;
+ left: 0;
+ border-radius: 2px;
+ box-shadow: var(--drop-shadow);
+ margin: var(--gutter);
+ padding: var(--gutter);
+ background-color: var(--blue-grey-25);
+ overflow: scroll;
+}
\ No newline at end of file
diff --git a/web/browser/components/mover/index.css b/web/browser/components/mover/index.css
deleted file mode 100644
index b220a53..0000000
--- a/web/browser/components/mover/index.css
+++ /dev/null
@@ -1,141 +0,0 @@
-/* Copyright 2015 The Vanadium Authors. All rights reserved. */
-/* Use of this source code is governed by a BSD-style */
-/* license that can be found in the LICENSE file. */
-
-@import "../base/variables.css";
-@import "../base/typography.css";
-
-.overlayBackground {
- position: fixed;
- top: 0;
- left: 0;
- background: rgba(0,0,0,0.75);
- z-index: 100;
- width: 100%;
- height: 100%;
-}
-
-.addButton {
- position: fixed;
- background-color: var(--deeporange-A200);
- color: var(--white);
- z-index: 150;
- width: 5%;
- height: 96%;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 2em;
-}
-
-.addBeforeButton {
- top: 2%;
- left: 2%;
-}
-
-.addAfterButton {
- top: 2%;
- left: 93%;
-}
-
-.textButton {
- background-color: var(--blue-grey-800);
- color: var(--white-54);
- z-index: 150;
- width: 10%;
- height: 3em;
- margin: 20px;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 1em;
-}
-
-.cancelBeforeAfterButton {
- position: fixed;
- top: 40%;
- left: 45%;
-}
-
-.movingThisDevice {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 100%;
- width: 100%;
-}
-
-.moveDeviceRow {
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 150px;
-}
-
-.deviceMovementSpace {
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- width: 100%;
-}
-
-.buttonRow {
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 150px;
-}
-
-.moveDevice {
- border: 2px solid var(--deeporange-A200);
- background-color: var(--grey-700);
- color: var(--deeporange-A200);
- z-index: 200;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 2em;
- height: 75px;
- width: 50px;
- margin: 20px;
-}
-
-.moveTarget {
- height: 25px;
- width: 25px;
- border-radius: 50%;
- background-color: var(--grey-700);
- z-index: 150;
- margin: 20px;
-}
-
-.overMoveTarget {
- height: 35px;
- width: 35px;
- background-color: var(--deeporange-A200);
- margin: 15px;
-}
-
-.deviceTile {
- border: 2px solid var(--white-54);
- background-color: var(--grey-700);
- color: var(--deeporange-A200);
- z-index: 150;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 2em;
- height: 75px;
- width: 50px;
- margin: 20px;
-}
-
-.linkLabel {
- color: var(--white-54);
-}
diff --git a/web/browser/components/mover/index.js b/web/browser/components/mover/index.js
deleted file mode 100644
index 80408b6..0000000
--- a/web/browser/components/mover/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-module.exports = {
- state: require('./state'),
- render: require('./render')
-};
diff --git a/web/browser/components/mover/render.js b/web/browser/components/mover/render.js
deleted file mode 100644
index c7233f1..0000000
--- a/web/browser/components/mover/render.js
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var insert = require('insert-css');
-var css = require('./index.css');
-var dragdrop = require('../../events/dragdrop');
-var droptarget = require('../../events/droptarget');
-
-module.exports = render;
-
-function render(state, channels) {
- insert(css);
-
- var moving = state.moving;
- var movingThisDevice = state.movingThisDevice;
-
- return h('.moveOverlay', {
- hidden: !moving,
- }, [
- h('.overlayBackground', [
- movingThisDevice ?
- hg.partial(moveThisDeviceControls, state, channels) :
- hg.partial(moveOtherDeviceControls, state, channels)
- ])
- ]);
-}
-
-function moveOtherDeviceControls(state, channels) {
- return h('.movingOtherDevice', [
- h('button.addBeforeButton .addButton', {
- 'ev-click': hg.send(channels.before)
- }, '+'),
- h('button.textButton .cancelBeforeAfterButton', {
- 'ev-click': hg.send(channels.cancel)
- }, 'Cancel'),
- h('button.addAfterButton .addButton', {
- 'ev-click': hg.send(channels.after)
- }, '+')
- ]);
-}
-
-function moveThisDeviceControls(state, channels) {
- var numDevicesInLinkedSet = state.numDevicesInLinkedSet;
- var firstPageNum = state.firstPageNum;
- var insertAtIndex = state.insertAtIndex;
- var highlightAtIndex = state.highlightAtIndex;
-
- var deviceTargetArray = [];
- // This is Used to adjust page numbers of devices after an insert
- var newPageOffset = 0;
-
- // Create a move target and device tile for each device already in the set
- for(var i = 0; i < numDevicesInLinkedSet; i++) {
- if (i === insertAtIndex) {
- // As long as the first page isn't page 1, put the new device before
- // the first device page
- var devicePageNum = firstPageNum + i;
- if (i === 0 && firstPageNum !== 1) {
- devicePageNum = firstPageNum - 1;
- } else {
- newPageOffset = 1;
- }
- deviceTargetArray.push(moveDevice(String(devicePageNum), channels));
- } else {
- deviceTargetArray.push(moveTarget(i,
- (i === highlightAtIndex),
- channels));
- }
- deviceTargetArray.push(h('.deviceTile',
- String(firstPageNum + i + newPageOffset)));
- }
-
- // Create the final item in the list of devices and targets
- // ---
- // If the insert index is equal to the number of devices in the set,
- // then it will be added at the end of the existing set. Otherwise
- // we show the final drop target
- var finalItem;
- if (insertAtIndex === numDevicesInLinkedSet) {
- var pageNum = firstPageNum + numDevicesInLinkedSet;
- finalItem = moveDevice(String(pageNum), channels);
- } else {
- var highlightLast = numDevicesInLinkedSet === highlightAtIndex;
- finalItem = moveTarget(numDevicesInLinkedSet, highlightLast, channels);
- }
- deviceTargetArray.push(finalItem);
-
- // The starting position of the device is above the device list, and
- // can either be the device or a move target
- var startNode;
- if (insertAtIndex < 0) {
- startNode = moveDevice('D', channels);
- } else {
- // We the number of devices + 1 as the index for the start move target
- // to enable proper highlighting
- var startTargetIndex = numDevicesInLinkedSet + 1;
- var highlightStart = (numDevicesInLinkedSet + 1) === highlightAtIndex;
- startNode = moveTarget(startTargetIndex, highlightStart, channels);
- }
-
- return h('.movingThisDevice', [
- h('.moveDeviceRow', [
- startNode,
- h('.linkLabel', (insertAtIndex < 0) ? 'Unlinked' : 'Linked')
- ]),
- h('.deviceMovementSpace', deviceTargetArray),
- h('.buttonRow', [
- h('button.textButton', {
- 'ev-click': hg.send(channels.cancel)
- }, 'Cancel'),
- h('button.textButton', {
- 'ev-click': hg.send(channels.commit)
- }, 'OK')
- ])
- ]);
-}
-
-function moveDevice(label, channels) {
- return h('.moveDevice', {
- draggable: true,
- 'ev-mousedown': dragdrop(channels.dragend)
- }, label);
-}
-
-function moveTarget(index, highlightFlag, channels) {
- var classNames = '.moveTarget' + (highlightFlag ? ' .overMoveTarget' : '');
-
- return h(classNames, {
- 'ev-dragenter': droptarget(channels.dragenter, { index: index }),
- 'ev-dragover': droptarget(channels.dragover, { index: index }),
- 'ev-dragleave': hg.send(channels.dragleave, { index: index }),
- 'ev-drop': hg.send(channels.selected, { index: index })
- },[]);
-}
diff --git a/web/browser/components/mover/state.js b/web/browser/components/mover/state.js
deleted file mode 100644
index 9cd5090..0000000
--- a/web/browser/components/mover/state.js
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var debug = require('debug')('reader:mover');
-
-module.exports = create;
-
-function create(options) {
- debug('creating Mover state: %o', options);
-
- var state = hg.state({
- error: hg.value(null),
- moving: hg.value(false),
- movingThisDevice: hg.value(false),
- insertAtIndex: hg.value(-1),
- highlightAtIndex: hg.value(-1),
- channels: {
- // User clicks to add/move the device before this one
- before: previous,
- // User clicks to add/move the device after this one
- after: next,
- // User clicks the cancel button
- cancel: cancel,
- // User selects location for new device manually (only on moving device)
- selected: link,
- // User clicks ok after selecting a new device location
- commit: commit,
- // Drop target channels
- dragenter: highlightTarget,
- dragleave: unhighlightTarget,
- // 'dragenter' and 'dragover' events that occur on valid drop targets
- // need to have event.preventDefault() called. This happens in the
- // droptarget event handler, which then broadcasts to a channel.
- // In the case of dropover, nothing else needs to happen, thus the
- // need for this channel handler.
- dragover: noop,
- // 'dragend' is also caught by the dragdrop event handler and broadcast
- // to a channel, but we do not currently have anything to do when this
- // happens. A successful drop is handled by the drop event, which is
- // communicated via the commit channel.
- dragend: noop
- },
- // These states should be linked into syncbase, but are here now until
- // that's ready
- numDevicesInLinkedSet: hg.value(4),
- firstPageNum: hg.value(2)
- });
-
- state.error(function(err) {
- if (!err) {
- return;
- }
-
- console.error('TODO: add an error component');
- console.error(err.stack);
- });
-
- return state;
-}
-
-function previous(state, data) {
- // TODO(jwnichols): This result of this decision needs to be determined and
- // then echoed to other devices
- state.moving.set(false);
-}
-
-function next(state, data) {
- // TODO(jwnichols): This result of this decision needs to be determined and
- // then echoed to other devices
- state.moving.set(false);
-}
-
-function cancel(state, data) {
- // Leave the move state without making any changes
- // TODO(jwnichols): This may need to be echoed to other devices
- state.moving.set(false);
-}
-
-function link(state, data) {
- if (data.index > state.numDevicesInLinkedSet()) {
- state.insertAtIndex.set(-1);
- } else {
- state.insertAtIndex.set(data.index);
- }
-}
-
-function commit(state, data) {
- // TODO(jwnichols): This result of this decision needs to be determined and
- // then echoed to other devices
- state.moving.set(false);
-}
-
-function highlightTarget(state, data) {
- state.highlightAtIndex.set(data.index);
-}
-
-function unhighlightTarget(state, data) {
- state.highlightAtIndex.set(-1);
-}
-
-function noop(state, data) {
- // The equivalent of a dev/null channel method
-}
diff --git a/web/browser/events/drag.js b/web/browser/events/drag.js
new file mode 100644
index 0000000..d5ba0ed
--- /dev/null
+++ b/web/browser/events/drag.js
@@ -0,0 +1,91 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var BaseEvent = require('mercury').BaseEvent;
+var extend = require('xtend');
+var hg = require('mercury');
+
+var delegator = hg.Delegator();
+
+// Listen to the low frequency, drag related events. The high frequency "data"
+// event is managed directly during event registration and removal in the drag
+// handler.
+delegator.listenTo('dragstart');
+delegator.listenTo('dragend');
+
+module.exports = BaseEvent(handleDrag); // jshint ignore:line
+
+// # var drag = require('./events/drag')
+//
+// Use as a drag handler in virtual-dom using either ev-event or ev-mousedown.
+// NOTE: The `draggable` attribute must be set to true on the vnode.
+//
+// h('.drag-me', {
+// draggable: true,
+// 'ev-event': drag(sink, { foo: 'bar' })
+// })
+//
+// The `drag` handler will manage all event registration and listening across
+// the lifespan of dragging on the target element. The broadcast to the `sink`
+// will happen with an extra attribute merged into the passed in data:
+//
+// * dragstart: { dragging: true }
+// * dragend: { dragging: false }
+//
+// Additionally, on the dragstart event any passed in data will be JSON encoded
+// and attached to the native event's dataTransfer object. This enables it's
+// retrieval from the drop DOM event (handled by `./events/drop`).
+//
+// NOTE: Currently this handle does not broadcast on high-frequency drag events,
+// the necessary code has been stubbed out below for easy modification in the
+// future.
+function handleDrag(ev, broadcast) {
+ // Only handle mousedown events, allows usage of ev-events for simplicity.
+ if (ev.type !== 'mousedown') {
+ return;
+ }
+
+ var data = this.data;
+ var element = ev.target;
+
+ delegator.listenTo('drag');
+ delegator.addEventListener(element, 'dragstart', dragstart);
+ delegator.addEventListener(element, 'drag', drag);
+ delegator.addEventListener(element, 'dragend', dragend);
+
+ // NOTE: Do not broadcast until the actual drag events have been fired.
+ return;
+
+ // Fired when the user starts dragging the target element.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/dragstart
+ function dragstart(event) {
+ // Using the raw DOM Event to access the DataTransfer object to set
+ // draggable data and the drag effect. This makes it possible for drop
+ // targets to access the data later.
+ //
+ // NOTE: dragover events do not have access to the data set below.
+ // SEE: https://goo.gl/fpwfP3
+ var raw = event._rawEvent;
+ raw.dataTransfer.setData('application/json', JSON.stringify(data));
+ raw.dataTransfer.effectAllowed = 'move';
+
+ broadcast(extend(data, { dragging: true }));
+ }
+
+ // Fires when the element is being dragged every few hundred ms. Currently a
+ // noop.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/drag
+ function drag(event) {}
+
+ // Fired when dragging has ended.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/dragend
+ function dragend(event) {
+ delegator.unlistenTo('drag');
+ delegator.removeEventListener(element, 'dragstart', dragstart);
+ delegator.removeEventListener(element, 'drag', drag);
+ delegator.removeEventListener(element, 'dragend', dragend);
+
+ broadcast(extend(data, { dragging: false }));
+ }
+}
diff --git a/web/browser/events/dragdrop.js b/web/browser/events/dragdrop.js
deleted file mode 100644
index 3e2a9c6..0000000
--- a/web/browser/events/dragdrop.js
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var BaseEvent = require('mercury').BaseEvent;
-
-module.exports = BaseEvent(dragHandler); // jshint ignore:line
-
-function dragHandler(ev, broadcast) {
- var delegator = hg.Delegator();
- var events = ['dragstart', 'dragenter', 'dragover', 'drop',
- 'dragend', 'dragleave'];
-
- function onstart(ev2) {
- // get the dataTransfer object out of the underlying event
- var dt = ev2._rawEvent.dataTransfer;
-
- // Set the data type to something unique to ensure our device cannot
- // be dragged to anything else. Despite the name, no real data is
- // being transferred here.
- dt.setData('application/vnd.vanadium.device', 'vanadium device');
-
- // Set the drag effect
- dt.effectAllowed = 'move';
-
- // Remove the drag start listener
- delegator.removeGlobalEventListener('dragstart', onstart);
- }
-
- function oncomplete(ev2) {
- // Merges passed in data and broadcasts
- broadcast(this.data);
-
- events.forEach(function(arg) { delegator.unlistenTo(arg); });
- delegator.removeGlobalEventListener('mouseup', oncomplete);
- delegator.removeGlobalEventListener('dragend', oncomplete);
- delegator.removeGlobalEventListener('dragstart', onstart);
- }
-
- events.forEach(function(arg) { delegator.listenTo(arg); });
- delegator.addGlobalEventListener('dragstart', onstart);
- delegator.addGlobalEventListener('mouseup', oncomplete);
- delegator.addGlobalEventListener('dragend', oncomplete);
-}
diff --git a/web/browser/events/dragover.js b/web/browser/events/dragover.js
new file mode 100644
index 0000000..474b4ae
--- /dev/null
+++ b/web/browser/events/dragover.js
@@ -0,0 +1,59 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var BaseEvent = require('mercury').BaseEvent;
+var extend = require('xtend');
+var hg = require('mercury');
+
+// Listen to the low frequency, drop related events.
+var delegator = hg.Delegator();
+delegator.listenTo('dragenter');
+delegator.listenTo('dragleave');
+delegator.listenTo('drop');
+
+module.exports = BaseEvent(handleDragover); // jshint ignore:line
+
+function handleDragover(ev, broadcast) {
+ // Only handle dragenter events, allows usage of ev-events for simplicity.
+ if (ev.type !== 'dragenter') {
+ return;
+ }
+
+ var element = ev.target;
+ var data = this.data;
+
+ delegator.listenTo('dragover');
+ delegator.addEventListener(element, 'dragover', dragover);
+ delegator.addEventListener(element, 'dragleave', dragleave);
+ delegator.addEventListener(element, 'drop', drop);
+
+ broadcast(extend(data, {
+ dragging: true
+ }));
+
+ return;
+
+ // Attached to the dragover event, this handler fires every few hundred ms.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/dragover
+ function dragover(event) {
+ // Prevent default in order to allow/enable the drop event to fire.
+ event.preventDefault();
+ }
+
+ function dragleave(event) {
+ delegator.unlistenTo('dragleave');
+ delegator.removeEventListener(element, 'dragover', dragover);
+ delegator.removeEventListener(element, 'dragleave', dragleave);
+ delegator.removeEventListener(element, 'drop', drop);
+
+ broadcast(extend(data, {
+ dragging: false
+ }));
+ }
+
+ function drop(event) {
+ dragleave(event);
+ }
+}
+
diff --git a/web/browser/events/drop.js b/web/browser/events/drop.js
new file mode 100644
index 0000000..997e41f
--- /dev/null
+++ b/web/browser/events/drop.js
@@ -0,0 +1,76 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var BaseEvent = require('mercury').BaseEvent;
+var extend = require('xtend');
+var hg = require('mercury');
+
+// Listen to the low frequency, drop related events.
+var delegator = hg.Delegator();
+delegator.listenTo('dragenter');
+delegator.listenTo('dragleave');
+delegator.listenTo('drop');
+
+module.exports = BaseEvent(handleDrop); // jshint ignore:line
+
+// # var drop = require('./events/drop')
+//
+// Use as a drop event handler in virtual-dom using ev-event or ev-dragenter.
+//
+// h('.drop-here', {
+// 'ev-event': drop(sink, data)
+// })
+//
+// The drop handler will only broadcast on a successful drop and will send a
+// merged data object containing any dragged data added by any drag handlers.
+function handleDrop(ev, broadcast) {
+ // Only handle dragenter events, allows usage of ev-events for simplicity.
+ if (ev.type !== 'dragenter') {
+ return;
+ }
+
+ var element = ev.target;
+ var data = this.data;
+
+ delegator.listenTo('dragover');
+ delegator.addEventListener(element, 'dragover', dragover);
+ delegator.addEventListener(element, 'dragleave', dragleave);
+ delegator.addEventListener(element, 'drop', drop);
+
+ // Attached to the dragover event, this handler fires every few hundred ms.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/dragover
+ function dragover(event) {
+ // Prevent default in order to allow/enable the drop event to fire.
+ event.preventDefault();
+ }
+
+ // Fires on on drop, will broadcast dropped data and detach all listeners.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/drop
+ function drop(event) {
+ var raw = event._rawEvent;
+ var json = raw.dataTransfer.getData('application/json');
+ var dropped;
+
+ try {
+ dropped = JSON.parse(json);
+ } catch (e) {
+ throw new Error('Error parsing JSON "' + json + '"');
+ }
+
+ // Since the passed data is derived from application state it is given
+ // precedence over the dataTransfer.
+ broadcast(extend(dropped, data));
+ dragleave(event);
+ }
+
+ // Fired when dragging is no longer over the droptarget.
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/Events/dragleave
+ function dragleave(event) {
+ delegator.unlistenTo('dragover');
+ delegator.removeEventListener(element, 'dragover', dragover);
+ delegator.removeEventListener(element, 'dragleave', dragleave);
+ delegator.removeEventListener(element, 'drop', drop);
+ }
+}
+
diff --git a/web/browser/events/droptarget.js b/web/browser/events/droptarget.js
deleted file mode 100644
index aa46035..0000000
--- a/web/browser/events/droptarget.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var BaseEvent = require('mercury').BaseEvent;
-
-module.exports = BaseEvent(dropTargetHandler); // jshint ignore:line
-
-function dropTargetHandler(ev, broadcast) {
- // Get the dataTransfer object out of the underlying event
- var dt = ev._rawEvent.dataTransfer;
-
- // Only fire this event if a Vanadium device is being dragged
- if (dt.types.indexOf('application/vnd.vanadium.device') >= 0) {
- ev.preventDefault();
- broadcast(this.data);
- }
-}
diff --git a/web/package.json b/web/package.json
index e8a8c4a..094463c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -12,7 +12,7 @@
},
"license": "BSD",
"devDependencies": {
- "browserify": "^11.0.1",
+ "browserify": "^12.0.1",
"concat-stream": "^1.5.0",
"dependency-check": "^2.5.1",
"disc": "^1.3.2",