@atlaskit/editor-plugin-annotation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +18 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +13 -0
- package/README.md +30 -0
- package/dist/cjs/commands/index.js +150 -0
- package/dist/cjs/commands/transform.js +86 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/nodeviews/index.js +59 -0
- package/dist/cjs/plugin.js +132 -0
- package/dist/cjs/pm-plugins/inline-comment.js +246 -0
- package/dist/cjs/pm-plugins/keymap.js +15 -0
- package/dist/cjs/pm-plugins/plugin-factory.js +107 -0
- package/dist/cjs/pm-plugins/reducer.js +84 -0
- package/dist/cjs/pm-plugins/types.js +17 -0
- package/dist/cjs/toolbar.js +59 -0
- package/dist/cjs/types.js +20 -0
- package/dist/cjs/ui/AnnotationViewWrapper.js +39 -0
- package/dist/cjs/ui/InlineCommentView.js +149 -0
- package/dist/cjs/utils.js +372 -0
- package/dist/es2019/commands/index.js +123 -0
- package/dist/es2019/commands/transform.js +64 -0
- package/dist/es2019/index.js +1 -0
- package/dist/es2019/nodeviews/index.js +31 -0
- package/dist/es2019/plugin.js +127 -0
- package/dist/es2019/pm-plugins/inline-comment.js +181 -0
- package/dist/es2019/pm-plugins/keymap.js +9 -0
- package/dist/es2019/pm-plugins/plugin-factory.js +108 -0
- package/dist/es2019/pm-plugins/reducer.js +94 -0
- package/dist/es2019/pm-plugins/types.js +11 -0
- package/dist/es2019/toolbar.js +53 -0
- package/dist/es2019/types.js +14 -0
- package/dist/es2019/ui/AnnotationViewWrapper.js +15 -0
- package/dist/es2019/ui/InlineCommentView.js +145 -0
- package/dist/es2019/utils.js +334 -0
- package/dist/esm/commands/index.js +143 -0
- package/dist/esm/commands/transform.js +80 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/nodeviews/index.js +52 -0
- package/dist/esm/plugin.js +120 -0
- package/dist/esm/pm-plugins/inline-comment.js +239 -0
- package/dist/esm/pm-plugins/keymap.js +9 -0
- package/dist/esm/pm-plugins/plugin-factory.js +101 -0
- package/dist/esm/pm-plugins/reducer.js +77 -0
- package/dist/esm/pm-plugins/types.js +11 -0
- package/dist/esm/toolbar.js +52 -0
- package/dist/esm/types.js +14 -0
- package/dist/esm/ui/AnnotationViewWrapper.js +32 -0
- package/dist/esm/ui/InlineCommentView.js +142 -0
- package/dist/esm/utils.js +345 -0
- package/dist/types/commands/index.d.ts +15 -0
- package/dist/types/commands/transform.d.ts +11 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/nodeviews/index.d.ts +11 -0
- package/dist/types/plugin.d.ts +6 -0
- package/dist/types/pm-plugins/inline-comment.d.ts +3 -0
- package/dist/types/pm-plugins/keymap.d.ts +3 -0
- package/dist/types/pm-plugins/plugin-factory.d.ts +2 -0
- package/dist/types/pm-plugins/reducer.d.ts +3 -0
- package/dist/types/pm-plugins/types.d.ts +78 -0
- package/dist/types/toolbar.d.ts +5 -0
- package/dist/types/types.d.ts +100 -0
- package/dist/types/ui/AnnotationViewWrapper.d.ts +10 -0
- package/dist/types/ui/InlineCommentView.d.ts +12 -0
- package/dist/types/utils.d.ts +44 -0
- package/dist/types-ts4.5/commands/index.d.ts +15 -0
- package/dist/types-ts4.5/commands/transform.d.ts +11 -0
- package/dist/types-ts4.5/index.d.ts +3 -0
- package/dist/types-ts4.5/nodeviews/index.d.ts +11 -0
- package/dist/types-ts4.5/plugin.d.ts +6 -0
- package/dist/types-ts4.5/pm-plugins/inline-comment.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/keymap.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/plugin-factory.d.ts +2 -0
- package/dist/types-ts4.5/pm-plugins/reducer.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/types.d.ts +78 -0
- package/dist/types-ts4.5/toolbar.d.ts +5 -0
- package/dist/types-ts4.5/types.d.ts +102 -0
- package/dist/types-ts4.5/ui/AnnotationViewWrapper.d.ts +10 -0
- package/dist/types-ts4.5/ui/InlineCommentView.d.ts +12 -0
- package/dist/types-ts4.5/utils.d.ts +44 -0
- package/package.json +106 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export let AnnotationSelectionType = /*#__PURE__*/function (AnnotationSelectionType) {
|
|
2
|
+
AnnotationSelectionType["INVALID"] = "invalid";
|
|
3
|
+
AnnotationSelectionType["DISABLED"] = "disabled";
|
|
4
|
+
AnnotationSelectionType["VALID"] = "valid";
|
|
5
|
+
return AnnotationSelectionType;
|
|
6
|
+
}({}); // Annotation can be created
|
|
7
|
+
const prefix = 'ak-editor-annotation';
|
|
8
|
+
export const AnnotationTestIds = {
|
|
9
|
+
prefix,
|
|
10
|
+
floatingComponent: `${prefix}-floating-component`,
|
|
11
|
+
floatingToolbarCreateButton: `${prefix}-toolbar-create-button`,
|
|
12
|
+
componentSave: `${prefix}-dummy-save-button`,
|
|
13
|
+
componentClose: `${prefix}-dummy-close-button`
|
|
14
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
// eslint-disable-next-line @repo/internal/react/no-class-components
|
|
3
|
+
export class AnnotationViewWrapper extends React.PureComponent {
|
|
4
|
+
componentDidMount() {
|
|
5
|
+
const {
|
|
6
|
+
onViewed
|
|
7
|
+
} = this.props;
|
|
8
|
+
if (onViewed) {
|
|
9
|
+
onViewed();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
render() {
|
|
13
|
+
return this.props.children;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, CONTENT_COMPONENT, EVENT_TYPE, RESOLVE_METHOD } from '@atlaskit/editor-common/analytics';
|
|
3
|
+
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
|
|
4
|
+
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
5
|
+
import { closeComponent, createAnnotation, removeInlineCommentNearSelection, setInlineCommentDraftState, updateInlineCommentResolvedState } from '../commands';
|
|
6
|
+
import { AnnotationTestIds } from '../types';
|
|
7
|
+
import { getAllAnnotations, getAnnotationViewKey, getPluginState, getSelectionPositions } from '../utils';
|
|
8
|
+
import { AnnotationViewWrapper } from './AnnotationViewWrapper';
|
|
9
|
+
const findPosForDOM = sel => {
|
|
10
|
+
const {
|
|
11
|
+
$from,
|
|
12
|
+
from
|
|
13
|
+
} = sel;
|
|
14
|
+
|
|
15
|
+
// Retrieve current TextNode
|
|
16
|
+
const index = $from.index();
|
|
17
|
+
const node = index < $from.parent.childCount && $from.parent.child(index);
|
|
18
|
+
|
|
19
|
+
// Right edge of a mark.
|
|
20
|
+
if (!node && $from.nodeBefore && $from.nodeBefore.isText && $from.nodeBefore.marks.find(mark => mark.type.name === 'annotation')) {
|
|
21
|
+
return from - 1;
|
|
22
|
+
}
|
|
23
|
+
return from;
|
|
24
|
+
};
|
|
25
|
+
export function InlineCommentView({
|
|
26
|
+
providers,
|
|
27
|
+
editorView,
|
|
28
|
+
editorAnalyticsAPI,
|
|
29
|
+
dispatchAnalyticsEvent
|
|
30
|
+
}) {
|
|
31
|
+
// As inlineComment is the only annotation present, this function is not generic
|
|
32
|
+
const {
|
|
33
|
+
inlineComment: inlineCommentProvider
|
|
34
|
+
} = providers;
|
|
35
|
+
const {
|
|
36
|
+
state,
|
|
37
|
+
dispatch
|
|
38
|
+
} = editorView;
|
|
39
|
+
const {
|
|
40
|
+
createComponent: CreateComponent,
|
|
41
|
+
viewComponent: ViewComponent
|
|
42
|
+
} = inlineCommentProvider;
|
|
43
|
+
const inlineCommentState = getPluginState(state);
|
|
44
|
+
const {
|
|
45
|
+
bookmark,
|
|
46
|
+
selectedAnnotations,
|
|
47
|
+
annotations,
|
|
48
|
+
isInlineCommentViewClosed
|
|
49
|
+
} = inlineCommentState || {};
|
|
50
|
+
const annotationsList = getAllAnnotations(editorView.state.doc);
|
|
51
|
+
const selection = getSelectionPositions(state, inlineCommentState);
|
|
52
|
+
const position = findPosForDOM(selection);
|
|
53
|
+
let dom;
|
|
54
|
+
try {
|
|
55
|
+
dom = findDomRefAtPos(position, editorView.domAtPos.bind(editorView));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.warn(error);
|
|
59
|
+
if (dispatchAnalyticsEvent) {
|
|
60
|
+
const payload = {
|
|
61
|
+
action: ACTION.ERRORED,
|
|
62
|
+
actionSubject: ACTION_SUBJECT.CONTENT_COMPONENT,
|
|
63
|
+
eventType: EVENT_TYPE.OPERATIONAL,
|
|
64
|
+
attributes: {
|
|
65
|
+
component: CONTENT_COMPONENT.INLINE_COMMENT,
|
|
66
|
+
selection: selection.toJSON(),
|
|
67
|
+
position,
|
|
68
|
+
docSize: editorView.state.doc.nodeSize,
|
|
69
|
+
error: error.toString()
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
dispatchAnalyticsEvent(payload);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!dom) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create Component
|
|
80
|
+
if (bookmark) {
|
|
81
|
+
if (!CreateComponent) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//getting all text between bookmarked positions
|
|
86
|
+
const textSelection = state.doc.textBetween(selection.from, selection.to);
|
|
87
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
88
|
+
"data-testid": AnnotationTestIds.floatingComponent,
|
|
89
|
+
"data-editor-popup": "true"
|
|
90
|
+
}, /*#__PURE__*/React.createElement(CreateComponent, {
|
|
91
|
+
dom: dom,
|
|
92
|
+
textSelection: textSelection,
|
|
93
|
+
onCreate: id => {
|
|
94
|
+
createAnnotation(editorAnalyticsAPI)(id)(editorView.state, editorView.dispatch);
|
|
95
|
+
!editorView.hasFocus() && editorView.focus();
|
|
96
|
+
},
|
|
97
|
+
onClose: () => {
|
|
98
|
+
setInlineCommentDraftState(editorAnalyticsAPI)(false)(editorView.state, editorView.dispatch);
|
|
99
|
+
!editorView.hasFocus() && editorView.focus();
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// View Component
|
|
105
|
+
const activeAnnotations = (selectedAnnotations === null || selectedAnnotations === void 0 ? void 0 : selectedAnnotations.filter(mark => annotations && annotations[mark.id] === false)) || [];
|
|
106
|
+
if (!ViewComponent || activeAnnotations.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const onAnnotationViewed = () => {
|
|
110
|
+
if (!dispatchAnalyticsEvent) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// fire analytics
|
|
114
|
+
const payload = {
|
|
115
|
+
action: ACTION.VIEWED,
|
|
116
|
+
actionSubject: ACTION_SUBJECT.ANNOTATION,
|
|
117
|
+
actionSubjectId: ACTION_SUBJECT_ID.INLINE_COMMENT,
|
|
118
|
+
eventType: EVENT_TYPE.TRACK,
|
|
119
|
+
attributes: {
|
|
120
|
+
overlap: activeAnnotations.length ? activeAnnotations.length - 1 : 0
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
dispatchAnalyticsEvent(payload);
|
|
124
|
+
};
|
|
125
|
+
if (getBooleanFF('platform.editor.annotation.decouple-inline-comment-closed_flmox') && isInlineCommentViewClosed || !selectedAnnotations) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return /*#__PURE__*/React.createElement(AnnotationViewWrapper, {
|
|
129
|
+
"data-editor-popup": "true",
|
|
130
|
+
"data-testid": AnnotationTestIds.floatingComponent,
|
|
131
|
+
key: getAnnotationViewKey(activeAnnotations),
|
|
132
|
+
onViewed: onAnnotationViewed
|
|
133
|
+
}, /*#__PURE__*/React.createElement(ViewComponent, {
|
|
134
|
+
annotationsList: annotationsList,
|
|
135
|
+
annotations: activeAnnotations,
|
|
136
|
+
dom: dom,
|
|
137
|
+
onDelete: id => removeInlineCommentNearSelection(id)(state, dispatch),
|
|
138
|
+
onResolve: id => updateInlineCommentResolvedState(editorAnalyticsAPI)({
|
|
139
|
+
[id]: true
|
|
140
|
+
}, RESOLVE_METHOD.COMPONENT)(editorView.state, editorView.dispatch),
|
|
141
|
+
onClose: () => {
|
|
142
|
+
closeComponent()(editorView.state, editorView.dispatch);
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { AnnotationTypes } from '@atlaskit/adf-schema';
|
|
2
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
3
|
+
import { AnnotationSharedClassNames } from '@atlaskit/editor-common/styles';
|
|
4
|
+
import { canApplyAnnotationOnRange, containsAnyAnnotations, getAnnotationIdsFromRange, hasAnnotationMark, isParagraph, isText } from '@atlaskit/editor-common/utils';
|
|
5
|
+
import { AllSelection, PluginKey, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
6
|
+
import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
7
|
+
import { AnnotationSelectionType } from './types';
|
|
8
|
+
export { hasAnnotationMark, containsAnyAnnotations };
|
|
9
|
+
function sum(arr, f) {
|
|
10
|
+
return arr.reduce((val, x) => val + f(x), 0);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Finds the marks in the nodes to the left and right.
|
|
14
|
+
* @param $pos Position to center search around
|
|
15
|
+
*/
|
|
16
|
+
export const surroundingMarks = $pos => {
|
|
17
|
+
const {
|
|
18
|
+
nodeBefore,
|
|
19
|
+
nodeAfter
|
|
20
|
+
} = $pos;
|
|
21
|
+
const markNodeBefore = nodeBefore && $pos.doc.nodeAt(Math.max(0, $pos.pos - nodeBefore.nodeSize - 1));
|
|
22
|
+
const markNodeAfter = nodeAfter && $pos.doc.nodeAt($pos.pos + nodeAfter.nodeSize);
|
|
23
|
+
return [markNodeBefore && markNodeBefore.marks || [], markNodeAfter && markNodeAfter.marks || []];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Finds annotation marks, and returns their IDs.
|
|
28
|
+
* @param marks Array of marks to search in
|
|
29
|
+
*/
|
|
30
|
+
const filterAnnotationIds = marks => {
|
|
31
|
+
if (!marks.length) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const {
|
|
35
|
+
annotation
|
|
36
|
+
} = marks[0].type.schema.marks;
|
|
37
|
+
return marks.filter(mark => mark.type === annotation).map(mark => mark.attrs.id);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Re-orders the annotation array based on the order in the document.
|
|
42
|
+
*
|
|
43
|
+
* This places the marks that do not appear in the surrounding nodes
|
|
44
|
+
* higher in the list. That is, the inner-most one appears first.
|
|
45
|
+
*
|
|
46
|
+
* Undo, for example, can re-order annotation marks in the document.
|
|
47
|
+
* @param annotations annotation metadata
|
|
48
|
+
* @param $from location to look around (usually the selection)
|
|
49
|
+
*/
|
|
50
|
+
const reorderAnnotations = (annotations, $from) => {
|
|
51
|
+
const idSet = surroundingMarks($from).map(filterAnnotationIds);
|
|
52
|
+
annotations.sort((a, b) => sum(idSet, ids => ids.indexOf(a.id)) - sum(idSet, ids => ids.indexOf(b.id)));
|
|
53
|
+
};
|
|
54
|
+
export const getAllAnnotations = doc => {
|
|
55
|
+
const allAnnotationIds = new Set();
|
|
56
|
+
doc.descendants(node => {
|
|
57
|
+
node.marks.filter(mark => mark.type.name === 'annotation')
|
|
58
|
+
// filter out annotations with invalid attributes as they cause errors when interacting with them
|
|
59
|
+
.filter(validateAnnotationMark).forEach(m => allAnnotationIds.add(m.attrs.id));
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
return Array.from(allAnnotationIds);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* verifies if annotation mark contains valid attributes
|
|
67
|
+
*/
|
|
68
|
+
const validateAnnotationMark = annotationMark => {
|
|
69
|
+
const {
|
|
70
|
+
id,
|
|
71
|
+
annotationType
|
|
72
|
+
} = annotationMark.attrs;
|
|
73
|
+
return validateAnnotationId(id) && validateAnnotationType(annotationType);
|
|
74
|
+
function validateAnnotationId(id) {
|
|
75
|
+
if (!id || typeof id !== 'string') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const invalidIds = ['null', 'undefined'];
|
|
79
|
+
return !invalidIds.includes(id.toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
function validateAnnotationType(type) {
|
|
82
|
+
if (!type || typeof type !== 'string') {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const allowedTypes = Object.values(AnnotationTypes);
|
|
86
|
+
return allowedTypes.includes(type);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* add decoration for the comment selection in draft state
|
|
92
|
+
* (when creating new comment)
|
|
93
|
+
*/
|
|
94
|
+
export const addDraftDecoration = (start, end) => {
|
|
95
|
+
return Decoration.inline(start, end, {
|
|
96
|
+
class: `${AnnotationSharedClassNames.draft}`
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
export const getAnnotationViewKey = annotations => {
|
|
100
|
+
const keys = annotations.map(mark => mark.id).join('_');
|
|
101
|
+
return `view-annotation-wrapper_${keys}`;
|
|
102
|
+
};
|
|
103
|
+
export const findAnnotationsInSelection = (selection, doc) => {
|
|
104
|
+
const {
|
|
105
|
+
empty,
|
|
106
|
+
$anchor,
|
|
107
|
+
anchor
|
|
108
|
+
} = selection;
|
|
109
|
+
// Only detect annotations on caret selection
|
|
110
|
+
if (!empty || !doc) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const node = doc.nodeAt(anchor);
|
|
114
|
+
if (!node && !$anchor.nodeBefore) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const annotationMark = doc.type.schema.marks.annotation;
|
|
118
|
+
const nodeBefore = $anchor.nodeBefore;
|
|
119
|
+
const anchorAnnotationMarks = node && node.marks || [];
|
|
120
|
+
let marks = [];
|
|
121
|
+
if (annotationMark.isInSet(anchorAnnotationMarks)) {
|
|
122
|
+
marks = anchorAnnotationMarks;
|
|
123
|
+
} else if (nodeBefore && annotationMark.isInSet(nodeBefore.marks)) {
|
|
124
|
+
marks = nodeBefore.marks;
|
|
125
|
+
}
|
|
126
|
+
const annotations = marks.filter(mark => mark.type.name === 'annotation').map(mark => ({
|
|
127
|
+
id: mark.attrs.id,
|
|
128
|
+
type: mark.attrs.annotationType
|
|
129
|
+
}));
|
|
130
|
+
reorderAnnotations(annotations, $anchor);
|
|
131
|
+
return annotations;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* get selection from position to apply new comment for
|
|
136
|
+
* @return bookmarked positions if they exists, otherwise current selection positions
|
|
137
|
+
*/
|
|
138
|
+
export function getSelectionPositions(editorState, inlineCommentState) {
|
|
139
|
+
const {
|
|
140
|
+
bookmark
|
|
141
|
+
} = inlineCommentState || {};
|
|
142
|
+
// get positions via saved bookmark if it is available
|
|
143
|
+
// this is to make comments box positioned relative to temporary highlight rather then current selection
|
|
144
|
+
if (bookmark) {
|
|
145
|
+
return bookmark.resolve(editorState.doc);
|
|
146
|
+
}
|
|
147
|
+
return editorState.selection;
|
|
148
|
+
}
|
|
149
|
+
export const inlineCommentPluginKey = new PluginKey('inlineCommentPluginKey');
|
|
150
|
+
export const getPluginState = state => {
|
|
151
|
+
return inlineCommentPluginKey.getState(state);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* get number of unique annotations within current selection
|
|
156
|
+
*/
|
|
157
|
+
const getAnnotationsInSelectionCount = state => {
|
|
158
|
+
const {
|
|
159
|
+
from,
|
|
160
|
+
to
|
|
161
|
+
} = state.selection;
|
|
162
|
+
const annotations = getAnnotationIdsFromRange({
|
|
163
|
+
from,
|
|
164
|
+
to
|
|
165
|
+
}, state.doc, state.schema);
|
|
166
|
+
return annotations.length;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* get payload for the open/close analytics event
|
|
171
|
+
*/
|
|
172
|
+
export const getDraftCommandAnalyticsPayload = (drafting, inputMethod) => {
|
|
173
|
+
const payload = state => {
|
|
174
|
+
let attributes = {};
|
|
175
|
+
if (drafting) {
|
|
176
|
+
attributes = {
|
|
177
|
+
inputMethod,
|
|
178
|
+
overlap: getAnnotationsInSelectionCount(state)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
action: drafting ? ACTION.OPENED : ACTION.CLOSED,
|
|
183
|
+
actionSubject: ACTION_SUBJECT.ANNOTATION,
|
|
184
|
+
actionSubjectId: ACTION_SUBJECT_ID.INLINE_COMMENT,
|
|
185
|
+
eventType: EVENT_TYPE.TRACK,
|
|
186
|
+
attributes
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
return payload;
|
|
190
|
+
};
|
|
191
|
+
export const isSelectionValid = state => {
|
|
192
|
+
const {
|
|
193
|
+
selection
|
|
194
|
+
} = state;
|
|
195
|
+
const {
|
|
196
|
+
disallowOnWhitespace
|
|
197
|
+
} = getPluginState(state) || {};
|
|
198
|
+
if (selection.empty || !(selection instanceof TextSelection || selection instanceof AllSelection)) {
|
|
199
|
+
return AnnotationSelectionType.INVALID;
|
|
200
|
+
}
|
|
201
|
+
const containsInvalidNodes = hasInvalidNodes(state);
|
|
202
|
+
|
|
203
|
+
// A selection that only covers 1 pos, and is an invalid node
|
|
204
|
+
// e.g. a text selection over a mention
|
|
205
|
+
if (containsInvalidNodes && selection.to - selection.from === 1) {
|
|
206
|
+
return AnnotationSelectionType.INVALID;
|
|
207
|
+
}
|
|
208
|
+
if (containsInvalidNodes) {
|
|
209
|
+
return AnnotationSelectionType.DISABLED;
|
|
210
|
+
}
|
|
211
|
+
if (disallowOnWhitespace && hasInvalidWhitespaceNode(selection, state.schema)) {
|
|
212
|
+
return AnnotationSelectionType.INVALID;
|
|
213
|
+
}
|
|
214
|
+
if (isEmptyTextSelection(selection, state.schema)) {
|
|
215
|
+
return AnnotationSelectionType.INVALID;
|
|
216
|
+
}
|
|
217
|
+
return AnnotationSelectionType.VALID;
|
|
218
|
+
};
|
|
219
|
+
export const hasInvalidNodes = state => {
|
|
220
|
+
const {
|
|
221
|
+
selection,
|
|
222
|
+
doc,
|
|
223
|
+
schema
|
|
224
|
+
} = state;
|
|
225
|
+
return !canApplyAnnotationOnRange({
|
|
226
|
+
from: selection.from,
|
|
227
|
+
to: selection.to
|
|
228
|
+
}, doc, schema);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Checks if selection contains only empty text
|
|
233
|
+
* e.g. when you select across multiple empty paragraphs
|
|
234
|
+
*/
|
|
235
|
+
function isEmptyTextSelection(selection, schema) {
|
|
236
|
+
const {
|
|
237
|
+
text,
|
|
238
|
+
paragraph
|
|
239
|
+
} = schema.nodes;
|
|
240
|
+
let hasContent = false;
|
|
241
|
+
selection.content().content.descendants(node => {
|
|
242
|
+
// for empty paragraph - consider empty (nothing to comment on)
|
|
243
|
+
if (node.type === paragraph && !node.content.size) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
// for not a text or nonempty text - consider nonempty (can comment if the node is supported for annotations)
|
|
247
|
+
if (node.type !== text || !node.textContent) {
|
|
248
|
+
hasContent = true;
|
|
249
|
+
}
|
|
250
|
+
return !hasContent;
|
|
251
|
+
});
|
|
252
|
+
return !hasContent;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Checks if any of the nodes in a given selection are completely whitespace
|
|
257
|
+
* This is to conform to Confluence annotation specifications
|
|
258
|
+
*/
|
|
259
|
+
export function hasInvalidWhitespaceNode(selection, schema) {
|
|
260
|
+
let foundInvalidWhitespace = false;
|
|
261
|
+
const content = selection.content().content;
|
|
262
|
+
content.descendants(node => {
|
|
263
|
+
if (isText(node, schema)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (node.textContent.trim() === '') {
|
|
267
|
+
// Trailing new lines do not result in the annotation spanning into
|
|
268
|
+
// the trailing new line so can be ignored when looking for invalid
|
|
269
|
+
// whitespace nodes.
|
|
270
|
+
const nodeIsTrailingNewLine =
|
|
271
|
+
// it is the final node
|
|
272
|
+
node.eq(content.lastChild) &&
|
|
273
|
+
// and there are multiple nodes
|
|
274
|
+
!node.eq(content.firstChild) &&
|
|
275
|
+
// and it is a paragraph node
|
|
276
|
+
isParagraph(node, schema);
|
|
277
|
+
if (!nodeIsTrailingNewLine) {
|
|
278
|
+
foundInvalidWhitespace = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return !foundInvalidWhitespace;
|
|
282
|
+
});
|
|
283
|
+
return foundInvalidWhitespace;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/*
|
|
287
|
+
* verifies that the annotation exists by the given id
|
|
288
|
+
*/
|
|
289
|
+
export function annotationExists(annotationId, state) {
|
|
290
|
+
const commentsPluginState = getPluginState(state);
|
|
291
|
+
return !!(commentsPluginState !== null && commentsPluginState !== void 0 && commentsPluginState.annotations) && Object.keys(commentsPluginState.annotations).includes(annotationId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* remove annotations that dont exsist in plugin state from slice
|
|
296
|
+
*/
|
|
297
|
+
export function stripNonExistingAnnotations(slice, state) {
|
|
298
|
+
if (!slice.content.size) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
slice.content.forEach(node => {
|
|
302
|
+
stripNonExistingAnnotationsFromNode(node, state);
|
|
303
|
+
node.content.descendants(node => {
|
|
304
|
+
stripNonExistingAnnotationsFromNode(node, state);
|
|
305
|
+
return true;
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/*
|
|
311
|
+
* remove annotations that dont exsist in plugin state
|
|
312
|
+
* from node
|
|
313
|
+
*/
|
|
314
|
+
function stripNonExistingAnnotationsFromNode(node, state) {
|
|
315
|
+
if (hasAnnotationMark(node, state)) {
|
|
316
|
+
node.marks = node.marks.filter(mark => {
|
|
317
|
+
if (mark.type.name === 'annotation') {
|
|
318
|
+
return annotationExists(mark.attrs.id, state);
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return node;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Compares two sets of annotationInfos to see if the annotations have changed
|
|
328
|
+
* This function assumes annotations will have unique id's for simplicity
|
|
329
|
+
*/
|
|
330
|
+
export function isSelectedAnnotationsChanged(oldSelectedAnnotations, newSelectedAnnotations) {
|
|
331
|
+
return newSelectedAnnotations.length !== oldSelectedAnnotations.length ||
|
|
332
|
+
// assuming annotations have unique id's for simplicity
|
|
333
|
+
newSelectedAnnotations.some(annotation => !oldSelectedAnnotations.find(pluginStateAnnotation => annotation.id === pluginStateAnnotation.id && annotation.type === pluginStateAnnotation.type));
|
|
334
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import { AnnotationTypes } from '@atlaskit/adf-schema';
|
|
3
|
+
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
4
|
+
import { createCommand } from '../pm-plugins/plugin-factory';
|
|
5
|
+
import { ACTIONS } from '../pm-plugins/types';
|
|
6
|
+
import { AnnotationSelectionType } from '../types';
|
|
7
|
+
import { getPluginState, isSelectionValid } from '../utils';
|
|
8
|
+
import transform from './transform';
|
|
9
|
+
export var updateInlineCommentResolvedState = function updateInlineCommentResolvedState(editorAnalyticsAPI) {
|
|
10
|
+
return function (partialNewState, resolveMethod) {
|
|
11
|
+
var command = {
|
|
12
|
+
type: ACTIONS.UPDATE_INLINE_COMMENT_STATE,
|
|
13
|
+
data: partialNewState
|
|
14
|
+
};
|
|
15
|
+
var allResolved = Object.values(partialNewState).every(function (state) {
|
|
16
|
+
return state;
|
|
17
|
+
});
|
|
18
|
+
if (resolveMethod && allResolved) {
|
|
19
|
+
return createCommand(command, transform.addResolveAnalytics(editorAnalyticsAPI)(resolveMethod));
|
|
20
|
+
}
|
|
21
|
+
return createCommand(command);
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export var closeComponent = function closeComponent() {
|
|
25
|
+
return createCommand({
|
|
26
|
+
type: ACTIONS.CLOSE_COMPONENT
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
export var clearDirtyMark = function clearDirtyMark() {
|
|
30
|
+
return createCommand({
|
|
31
|
+
type: ACTIONS.INLINE_COMMENT_CLEAR_DIRTY_MARK
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
export var removeInlineCommentNearSelection = function removeInlineCommentNearSelection(id) {
|
|
35
|
+
return function (state, dispatch) {
|
|
36
|
+
var tr = state.tr,
|
|
37
|
+
$from = state.selection.$from;
|
|
38
|
+
var annotationMarkType = state.schema.marks.annotation;
|
|
39
|
+
var hasAnnotation = $from.marks().some(function (mark) {
|
|
40
|
+
return mark.type === annotationMarkType;
|
|
41
|
+
});
|
|
42
|
+
if (!hasAnnotation) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// just remove entire mark from around the node
|
|
47
|
+
tr.removeMark($from.start(), $from.end(), annotationMarkType.create({
|
|
48
|
+
id: id,
|
|
49
|
+
type: AnnotationTypes.INLINE_COMMENT
|
|
50
|
+
}));
|
|
51
|
+
if (dispatch) {
|
|
52
|
+
dispatch(tr);
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
var getDraftCommandAction = function getDraftCommandAction(drafting) {
|
|
58
|
+
return function (editorState) {
|
|
59
|
+
// validate selection only when entering draft mode
|
|
60
|
+
if (drafting && isSelectionValid(editorState) !== AnnotationSelectionType.VALID) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
type: ACTIONS.SET_INLINE_COMMENT_DRAFT_STATE,
|
|
65
|
+
data: {
|
|
66
|
+
drafting: drafting,
|
|
67
|
+
editorState: editorState
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export var setInlineCommentDraftState = function setInlineCommentDraftState(editorAnalyticsAPI) {
|
|
73
|
+
return function (drafting) {
|
|
74
|
+
var inputMethod = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : INPUT_METHOD.TOOLBAR;
|
|
75
|
+
var commandAction = getDraftCommandAction(drafting);
|
|
76
|
+
return createCommand(commandAction, transform.addOpenCloseAnalytics(editorAnalyticsAPI)(drafting, inputMethod));
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export var addInlineComment = function addInlineComment(editorAnalyticsAPI) {
|
|
80
|
+
return function (id) {
|
|
81
|
+
var commandAction = function commandAction(editorState) {
|
|
82
|
+
return {
|
|
83
|
+
type: ACTIONS.ADD_INLINE_COMMENT,
|
|
84
|
+
data: {
|
|
85
|
+
drafting: false,
|
|
86
|
+
inlineComments: _defineProperty({}, id, false),
|
|
87
|
+
// Auto make the newly inserted comment selected.
|
|
88
|
+
// We move the selection to the head of the comment selection.
|
|
89
|
+
selectedAnnotations: [{
|
|
90
|
+
id: id,
|
|
91
|
+
type: AnnotationTypes.INLINE_COMMENT
|
|
92
|
+
}],
|
|
93
|
+
editorState: editorState
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
return createCommand(commandAction, transform.addInlineComment(editorAnalyticsAPI)(id));
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
export var updateMouseState = function updateMouseState(mouseData) {
|
|
101
|
+
return createCommand({
|
|
102
|
+
type: ACTIONS.INLINE_COMMENT_UPDATE_MOUSE_STATE,
|
|
103
|
+
data: {
|
|
104
|
+
mouseData: mouseData
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
export var setSelectedAnnotation = function setSelectedAnnotation(id) {
|
|
109
|
+
return createCommand({
|
|
110
|
+
type: ACTIONS.SET_SELECTED_ANNOTATION,
|
|
111
|
+
data: {
|
|
112
|
+
selectedAnnotations: [{
|
|
113
|
+
id: id,
|
|
114
|
+
type: AnnotationTypes.INLINE_COMMENT
|
|
115
|
+
}]
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
export var createAnnotation = function createAnnotation(editorAnalyticsAPI) {
|
|
120
|
+
return function (id) {
|
|
121
|
+
var annotationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : AnnotationTypes.INLINE_COMMENT;
|
|
122
|
+
return function (state, dispatch) {
|
|
123
|
+
// don't try to add if there are is no temp highlight bookmarked
|
|
124
|
+
var _ref = getPluginState(state) || {},
|
|
125
|
+
bookmark = _ref.bookmark;
|
|
126
|
+
if (!bookmark || !dispatch) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (annotationType === AnnotationTypes.INLINE_COMMENT) {
|
|
130
|
+
return addInlineComment(editorAnalyticsAPI)(id)(state, dispatch);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
export var setInlineCommentsVisibility = function setInlineCommentsVisibility(isVisible) {
|
|
137
|
+
return createCommand({
|
|
138
|
+
type: ACTIONS.INLINE_COMMENT_SET_VISIBLE,
|
|
139
|
+
data: {
|
|
140
|
+
isVisible: isVisible
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|