Skip to content

Commit

Permalink
Allow dropping urls, whole IIIF manifests, or images onto mirador
Browse files Browse the repository at this point in the history
  • Loading branch information
cbeer committed Jun 17, 2020
1 parent 0e06f5c commit b9eaa83
Show file tree
Hide file tree
Showing 8 changed files with 416 additions and 106 deletions.
69 changes: 69 additions & 0 deletions __tests__/src/components/IIIFDropTarget.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { handleDrop } from '../../../src/components/IIIFDropTarget';

const monitor = jest.fn();

describe('handleDrop', () => {
let onDrop;

beforeEach(() => {
onDrop = jest.fn();
});

it('handles url lists', () => {
const item = {
urls: [
'http://example.com/?manifest=http://example.com/iiif/1.json',
'http://example.com/?manifest=http://example.com/iiif/2.json&canvas=url',
],
};
const props = { onDrop };

handleDrop(item, monitor, props);
expect(onDrop).toHaveBeenCalledWith({ canvasId: null, manifestId: 'http://example.com/iiif/1.json' }, props, monitor);
expect(onDrop).toHaveBeenCalledWith({ canvasId: 'url', manifestId: 'http://example.com/iiif/2.json' }, props, monitor);
});

it('handles manifests', () => {
const file = new File(['{ "data": 123 }'], 'manifest.json', { type: 'application/json' });
const item = {
files: [
file,
],
};

const props = { onDrop };

const promise = handleDrop(item, monitor, props);

return promise.then(() => {
expect(onDrop).toHaveBeenCalledWith(
expect.objectContaining({ manifestJson: '{ "data": 123 }' }),
props,
monitor,
);
});
});

// jsdom doesn't load images.
xit('handles images by fabricating a temporary manifest', () => {
const arrayBuffer = Uint8Array.from(window.atob('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='), c => c.charCodeAt(0));
const file = new File(arrayBuffer, 'image.gif', { type: 'image/gif' });
const item = {
files: [
file,
],
};

const props = { onDrop };

const promise = handleDrop(item, monitor, props);

return promise.then(() => {
expect(onDrop).toHaveBeenCalledWith(
expect.objectContaining({ manifestJson: '{}' }),
props,
monitor,
);
});
});
});
59 changes: 42 additions & 17 deletions __tests__/src/components/Workspace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import WorkspaceMosaic from '../../../src/containers/WorkspaceMosaic';
import WorkspaceElastic from '../../../src/containers/WorkspaceElastic';
import Window from '../../../src/containers/Window';
import { Workspace } from '../../../src/components/Workspace';
import { IIIFDropTarget } from '../../../src/components/IIIFDropTarget';

/**
* Utility function to create a Worksapce
Expand All @@ -30,10 +31,12 @@ describe('Workspace', () => {
const wrapper = createWrapper({ workspaceType: 'elastic' });

expect(wrapper.matchesElement(
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<WorkspaceElastic />
</div>,
<IIIFDropTarget>
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<WorkspaceElastic />
</div>
</IIIFDropTarget>,
)).toBe(true);
});
});
Expand All @@ -42,33 +45,39 @@ describe('Workspace', () => {
const wrapper = createWrapper();

expect(wrapper.matchesElement(
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<WorkspaceMosaic />
</div>,
<IIIFDropTarget>
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<WorkspaceMosaic />
</div>
</IIIFDropTarget>,
)).toBe(true);
});
});
describe('if workspace type is unknown', () => {
it('should render <Window/> components as list', () => {
const wrapper = createWrapper({ workspaceType: 'bubu' });
expect(wrapper.matchesElement(
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<Window windowId="1" />
<Window windowId="2" />
</div>,
<IIIFDropTarget>
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<Window windowId="1" />
<Window windowId="2" />
</div>
</IIIFDropTarget>,
)).toBe(true);
});
});
describe('if any windows are maximized', () => {
it('should render only maximized <Window/> components', () => {
const wrapper = createWrapper({ maximizedWindowIds: ['1'] });
expect(wrapper.matchesElement(
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<Window windowId="1" className="mirador-workspace-maximized-window" />
</div>,
<IIIFDropTarget>
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Typography>miradorViewer</Typography>
<Window windowId="1" className="mirador-workspace-maximized-window" />
</div>
</IIIFDropTarget>,
)).toBe(true);
});
});
Expand Down Expand Up @@ -98,4 +107,20 @@ describe('Workspace', () => {
expect(wrapper.find('.mirador-workspace-with-control-panel').length).toBe(0);
});
});

describe('drag and drop', () => {
it('adds a new window', () => {
const canvasId = 'canvasId';
const manifestId = 'manifest.json';
const manifestJson = { data: '123' };

const addWindow = jest.fn();

const wrapper = createWrapper({ addWindow });

wrapper.find(IIIFDropTarget).simulate('drop', { canvasId, manifestId, manifestJson });

expect(addWindow).toHaveBeenCalledWith({ canvasId, manifest: manifestJson, manifestId });
});
});
});
28 changes: 28 additions & 0 deletions __tests__/src/components/WorkspaceAdd.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Typography from '@material-ui/core/Typography';
import { WorkspaceAdd } from '../../../src/components/WorkspaceAdd';
import ManifestListItem from '../../../src/containers/ManifestListItem';
import ManifestForm from '../../../src/containers/ManifestForm';
import { IIIFDropTarget } from '../../../src/components/IIIFDropTarget';

/** create wrapper */
function createWrapper(props) {
Expand Down Expand Up @@ -82,4 +83,31 @@ describe('WorkspaceAdd', () => {
wrapper.find(Drawer).find(ManifestForm).props().onCancel();
expect(wrapper.find(Drawer).props().open).toBe(false);
});

describe('drag and drop', () => {
it('adds a new catalog entry from a manifest', () => {
const manifestId = 'manifest.json';
const manifestJson = { data: '123' };

const addResource = jest.fn();

const wrapper = createWrapper({ addResource });

wrapper.find(IIIFDropTarget).simulate('drop', { manifestId, manifestJson });

expect(addResource).toHaveBeenCalledWith(manifestId, manifestJson, { provider: 'file' });
});

it('adds a new catalog entry from a manifestId', () => {
const manifestId = 'manifest.json';

const addResource = jest.fn();

const wrapper = createWrapper({ addResource });

wrapper.find(IIIFDropTarget).simulate('drop', { manifestId });

expect(addResource).toHaveBeenCalledWith(manifestId);
});
});
});
139 changes: 139 additions & 0 deletions src/components/IIIFDropTarget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { v4 as uuid } from 'uuid';
import { NativeTypes } from 'react-dnd-html5-backend';
import { useDrop } from 'react-dnd';

/** */
export const handleDrop = (item, monitor, props) => {
const { onDrop } = props;

if (item.urls) {
item.urls.forEach((str) => {
const url = new URL(str);
const manifestId = url.searchParams.get('manifest');
const canvasId = url.searchParams.get('canvas');

if (manifestId) onDrop({ canvasId, manifestId }, props, monitor);
});
}

if (item.files) {
const manifestFiles = item.files.filter(f => f.type === 'application/json');
const manifestPromises = manifestFiles.map(file => (
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => {
const manifestJson = reader.result;
const manifestId = uuid();

if (manifestJson) onDrop({ manifestId, manifestJson }, props, monitor);
resolve();
});
reader.readAsText(file);
})
));

const imageFiles = item.files.filter(({ type }) => type.startsWith('image/'));

let imagePromise;

if (imageFiles.length > 0) {
const id = uuid();
const imageData = imageFiles.map(file => (
new Promise((resolve, reject) => {
const reader = new FileReader();
console.log(file);
reader.addEventListener('load', () => {
const image = new Image();
image.addEventListener('load', () => {
resolve({
height: image.height,
name: file.name,
type: file.type,
url: reader.result,
width: image.width,
});
});
image.src = reader.result;
});
reader.readAsDataURL(file);
})
));

imagePromise = Promise.all(imageData).then((images) => {
const manifestJson = {
'@context': 'http://iiif.io/api/presentation/3/context.json',
id,
items: images.map(({
name, type, width, height, url,
}, index) => ({
height,
id: `${id}/canvas/${index}`,
items: [
{
id: `${id}/canvas/${index}/1`,
items: [{
body: {
format: type,
id: url,
type: 'Image',
},
height,
id: `${id}/canvas/${index}/1/image`,
motivation: 'painting',
target: `${id}/canvas/${index}/1`,
type: 'Annotation',
width,
}],
type: 'AnnotationPage',
},
],
label: name,
type: 'Canvas',
width,
})),
label: images[0].name,
type: 'Manifest',
};

const manifestId = uuid();
if (manifestJson) onDrop({ manifestId, manifestJson }, props, monitor);
});
}

return Promise.all([...manifestPromises, imagePromise]);
}

return undefined;
};

/** */
export const IIIFDropTarget = (props) => {
const { children, onDrop } = props;
const [{ canDrop, isOver }, drop] = useDrop({
accept: [NativeTypes.URL, NativeTypes.FILE],
collect: monitor => ({
canDrop: monitor.canDrop(),
isOver: monitor.isOver(),
}),
/** */
drop(item, monitor) {
if (!onDrop) return;
handleDrop(item, monitor, props);
},
});
// TODO: give some indication the app receives drops
const isActive = canDrop && isOver; // eslint-disable-line no-unused-vars

return (
<div ref={drop}>
{children}
</div>
);
};

IIIFDropTarget.propTypes = {
children: PropTypes.node.isRequired,
onDrop: PropTypes.func.isRequired,
};
Loading

0 comments on commit b9eaa83

Please sign in to comment.