Skip to content

Commit

Permalink
Merge pull request #1045 from 18F/prosemirror-links
Browse files Browse the repository at this point in the history
Prosemirror links
  • Loading branch information
cmc333333 authored Feb 28, 2018
2 parents a4a0e66 + a6c4304 commit b137538
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 8 deletions.
1 change: 1 addition & 0 deletions api/document/js/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface ApiContent {
inlines: ApiContent[];
text: string;
footnote_node?: ApiNode;
href?: string;
}

export class Api<T> {
Expand Down
1 change: 1 addition & 0 deletions api/document/js/__tests__/Api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const api = new Api<string>({
});

describe('fetch()', () => {
axios.get = jest.fn(async () => ({ data: '' }));
it('passes the correct args', () => {
api.fetch();
expect(axios.get).toHaveBeenCalledWith(
Expand Down
1 change: 1 addition & 0 deletions api/document/js/__tests__/menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('menu', () => {
'Undo last change',
'Redo last undone change',
'Append paragraph',
'Add or remove link',
'Append bullet list',
'Append ordered list',
'Outdent list',
Expand Down
50 changes: 49 additions & 1 deletion api/document/js/__tests__/parse-doc.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios';

import { ApiContent } from '../Api';
import { ApiContent, ApiNode } from '../Api';
import parseDoc, { convertContent } from '../parse-doc';
import { apiFactory } from '../serialize-doc';
import schema from '../schema';
Expand Down Expand Up @@ -117,6 +117,54 @@ describe('parseDoc()', () => {
expect(lis[1].content.child(1).type.name).toBe('para');
});

it('loads links', () => {
// This checks link parsing but doesn't check any of the interaction
// involved, such as making sure a prompt actually pops up in response
// to a button click.
const node: ApiNode = {
node_type: 'para',
content: [
{
content_type: '__text__',
text: 'Initial ',
inlines: [],
},
{
content_type: 'external_link',
href: 'http://example.org',
text: 'content.',
inlines: [{ content_type: '__text__', text: 'content.', inlines: [] }],
},

],
children: [],
};

const result = parseDoc(node); // The top-level node.
expect(result.type.name).toBe('para');
expect(result.content.size).toEqual('Initial content.'.length + 2); // Node boundaries add two.
expect(result.childCount).toEqual(1); // One inner node.

const paraText = result.child(0); // The inner paraText node.
expect(paraText.content.size).toEqual('Initial content.'.length);
expect(paraText.type.name).toEqual('paraText');
expect(paraText.childCount).toEqual(2); // The two inline children of the inner node.

const plainText = paraText.child(0); // The first inline child, plain text.
expect(plainText.type.name).toEqual('text');
expect(plainText.text).toEqual('Initial ');

const externalLink = paraText.child(1); // The second inline child, containing the link.
expect(externalLink.type.name).toEqual('text');
expect(externalLink.text).toEqual('content.');
expect(externalLink.marks.length).toEqual(1); // Just one link.

const mark = externalLink.marks[0]; // The link markup.
expect(mark.type.name).toEqual('external_link');
expect(mark.attrs.href).toEqual('http://example.org');

});

describe('unimplementedNode', () => {
it('saves original data', () => {
const node = {
Expand Down
46 changes: 43 additions & 3 deletions api/document/js/menu.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toggleMark } from 'prosemirror-commands';
import { menuBar, undoItem, redoItem, MenuItem, MenuItemSpec } from 'prosemirror-menu';

import { JsonApi } from './Api';
Expand All @@ -11,6 +12,7 @@ import {
outdentLi,
} from './commands';
import icons from './icons';
import schema from './schema';

function makeButton(content) {
return new MenuItem({
Expand All @@ -33,13 +35,14 @@ export default function menu(api: JsonApi) {
// prosemirror-menu. For more details, see:
//
// https://github.com/ProseMirror/prosemirror-menu/issues/12
undoItem as any as MenuItem,
redoItem as any as MenuItem,
undoItem as any as MenuItem, // title: 'Undo last change'
redoItem as any as MenuItem, // title: 'Redo last undone change'
makeButton({
label: 'P',
run: appendParagraphNear,
title: 'Append paragraph',
}),
linkItem(schema.marks.external_link), // title: 'Add or remove link'
makeButton({
icon: icons.newBulletList,
run: appendBulletListNear,
Expand Down Expand Up @@ -69,10 +72,47 @@ export default function menu(api: JsonApi) {
}),
makeButton({
label: 'Save then XML',
title: 'Save document then edit as XML',
run: makeSaveThenXml(api),
title: 'Save document then edit as XML',
}),
],
],
});
}

function markActive(state, type) {
const { from, $from, to, empty } = state.selection;
if (empty) {
return type.isInSet(state.storedMarks || $from.marks());
}
return state.doc.rangeHasMark(from, to, type);
}

function externalLink(state, dispatch, view, markType) {
// This function might belong in ./commands
if (markActive(state, markType)) {
toggleMark(markType)(state, dispatch);
return true;
}
// We need a replacement for prompt here.
toggleMark(schema.marks.external_link, {
href: prompt('URL', 'URL: '),
})(view.state, view.dispatch);
view.focus();
return true;
}

function linkItem(markType) {
return new MenuItem({
class: 'menuitem-clickable',
// These defaults are needed due to a doc issue. See
// https://github.com/ProseMirror/prosemirror-menu/issues/15
css: '',
execEvent: 'mousedown',
title: 'Add or remove link',
label: 'A',
active(state) { return markActive(state, markType); },
enable(state) { return !state.selection.empty; },
run(state, dispatch, view) { return externalLink(state, dispatch, view, markType); },
});
}
1 change: 1 addition & 0 deletions api/document/js/parse-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,5 @@ const NODE_TYPE_CONVERTERS: NodeConverterMap = {

const CONTENT_TYPE_CONVERTERS = {
unimplementedMark: content => factory.unimplementedMark(content),
external_link: content => factory.external_link(content.href),
};
14 changes: 12 additions & 2 deletions api/document/js/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,25 @@ const schema = new Schema({
},
toDOM(node) {
const nodeType = node.attrs.data.node_type || '[no-node-type]';
return ['div', { class: 'unimplemented' }, nodeType];
return ['div', { class: 'unimplemented whatever' }, nodeType];
},
},
...listSchemaNodes,
},
marks: {
external_link: {
attrs: {
href: {},
},
toDOM(data) {
return ['a', { class: `external-link`, href: data.attrs.href }];
},
},
unimplementedMark: {
attrs: {
data: {}, // will hold unrendered content
},
toDOM: () => ['span', { class: 'unimplemented' }],
toDOM: data => ['span', { class: `unimplemented` }],
},
},
});
Expand Down Expand Up @@ -151,6 +159,8 @@ export const factory = {
schema.nodes.policy.create({}, children || []),
sec: (children?: Node[] | Fragment) =>
schema.nodes.sec.create({}, children || []),
external_link: (href: string) =>
schema.marks.external_link.create({ href }),
unimplementedMark: (original: any) =>
schema.marks.unimplementedMark.create({ data: original }),
unimplementedNode: (original: any) =>
Expand Down
6 changes: 4 additions & 2 deletions api/document/js/serialize-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ function defaultNodeConverter(node: Node): ApiNode {
}

const MARK_CONVERTERS = {
unimplementedMark: node =>
apiFactory.content(node.type.name, node.attrs.data),
unimplementedMark: mark =>
apiFactory.content(mark.type.name, mark.attrs.data),
external_link: mark =>
apiFactory.content(mark.type.name, { href: mark.attrs.href }),
};


Expand Down

0 comments on commit b137538

Please sign in to comment.