@manuscripts/body-editor 3.13.19 → 3.15.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.
Files changed (52) hide show
  1. package/dist/cjs/commands.js +30 -2
  2. package/dist/cjs/components/authors-affiliations/AuthorsAndAffiliationsModals.js +1 -0
  3. package/dist/cjs/components/cross-ref-check-modal/CrossRefWarningModal.js +182 -0
  4. package/dist/cjs/components/cross-ref-check-modal/openModal.js +38 -0
  5. package/dist/cjs/components/views/DeleteSupplementDialog.js +75 -0
  6. package/dist/cjs/icons.js +2 -1
  7. package/dist/cjs/index.js +4 -1
  8. package/dist/cjs/lib/files.js +17 -5
  9. package/dist/cjs/lib/supplements.js +61 -0
  10. package/dist/cjs/plugins/cross-references.js +160 -0
  11. package/dist/cjs/versions.js +1 -1
  12. package/dist/cjs/views/caption_title.js +1 -0
  13. package/dist/cjs/views/supplement.js +21 -1
  14. package/dist/es/commands.js +26 -0
  15. package/dist/es/components/authors-affiliations/AuthorsAndAffiliationsModals.js +1 -0
  16. package/dist/es/components/cross-ref-check-modal/CrossRefWarningModal.js +142 -0
  17. package/dist/es/components/cross-ref-check-modal/openModal.js +31 -0
  18. package/dist/es/components/views/DeleteSupplementDialog.js +35 -0
  19. package/dist/es/icons.js +2 -1
  20. package/dist/es/index.js +2 -0
  21. package/dist/es/lib/files.js +17 -5
  22. package/dist/es/lib/supplements.js +55 -0
  23. package/dist/es/plugins/cross-references.js +162 -2
  24. package/dist/es/versions.js +1 -1
  25. package/dist/es/views/caption_title.js +1 -0
  26. package/dist/es/views/supplement.js +22 -2
  27. package/dist/types/commands.d.ts +2 -0
  28. package/dist/types/components/authors-affiliations/AuthorsAndAffiliationsModals.d.ts +1 -1
  29. package/dist/types/components/cross-ref-check-modal/CrossRefWarningModal.d.ts +29 -0
  30. package/dist/types/components/cross-ref-check-modal/openModal.d.ts +19 -0
  31. package/dist/types/components/views/DeleteSupplementDialog.d.ts +2 -0
  32. package/dist/types/icons.d.ts +1 -0
  33. package/dist/types/index.d.ts +2 -0
  34. package/dist/types/lib/files.d.ts +2 -0
  35. package/dist/types/lib/supplements.d.ts +30 -0
  36. package/dist/types/versions.d.ts +1 -1
  37. package/package.json +2 -2
  38. package/src/commands.ts +42 -0
  39. package/src/components/authors-affiliations/AuthorsAndAffiliationsModals.tsx +2 -0
  40. package/src/components/cross-ref-check-modal/CrossRefWarningModal.tsx +223 -0
  41. package/src/components/cross-ref-check-modal/openModal.ts +50 -0
  42. package/src/components/views/DeleteSupplementDialog.tsx +88 -0
  43. package/src/icons.ts +2 -0
  44. package/src/index.ts +2 -0
  45. package/src/lib/files.ts +20 -6
  46. package/src/lib/supplements.ts +97 -0
  47. package/src/plugins/cross-references.ts +232 -6
  48. package/src/versions.ts +1 -1
  49. package/src/views/caption_title.ts +2 -0
  50. package/src/views/supplement.ts +27 -2
  51. package/src/views/supplements.ts +2 -1
  52. package/styles/Editor.css +13 -0
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MATHJAX_VERSION = exports.VERSION = void 0;
4
- exports.VERSION = '3.13.19';
4
+ exports.VERSION = '3.15.0';
5
5
  exports.MATHJAX_VERSION = '3.2.2';
@@ -23,6 +23,7 @@ class CaptionTitleView extends base_node_view_1.BaseNodeView {
23
23
  super(...arguments);
24
24
  this.initialise = () => {
25
25
  this.createDOM();
26
+ this.updateContents();
26
27
  };
27
28
  }
28
29
  createDOM() {
@@ -20,6 +20,7 @@ const style_guide_1 = require("@manuscripts/style-guide");
20
20
  const server_1 = require("react-dom/server");
21
21
  const icons_1 = require("../icons");
22
22
  const doc_1 = require("../lib/doc");
23
+ const url_1 = require("../lib/url");
23
24
  const base_node_view_1 = require("./base_node_view");
24
25
  const creators_1 = require("./creators");
25
26
  class SupplementView extends base_node_view_1.BaseNodeView {
@@ -143,8 +144,27 @@ class SupplementView extends base_node_view_1.BaseNodeView {
143
144
  this.supplementInfoEl = document.createElement('div');
144
145
  this.supplementInfoEl.classList.add('supplement-file-info');
145
146
  this.supplementInfoEl.contentEditable = 'false';
147
+ const href = this.node.attrs.href;
148
+ if ((0, url_1.allowedHref)(href)) {
149
+ const iconElement = document.createElement('span');
150
+ iconElement.classList.add('supplement-file-icon');
151
+ iconElement.innerHTML = icons_1.webLinkIcon;
152
+ this.supplementInfoEl.appendChild(iconElement);
153
+ const urlLink = document.createElement('a');
154
+ urlLink.classList.add('supplement-weblink-url');
155
+ urlLink.textContent = href;
156
+ if ((0, url_1.allowedHref)(href)) {
157
+ urlLink.href = href;
158
+ urlLink.target = '_blank';
159
+ urlLink.rel = 'noopener noreferrer';
160
+ }
161
+ urlLink.addEventListener('mousedown', (e) => e.stopPropagation());
162
+ this.supplementInfoEl.appendChild(urlLink);
163
+ this.dom.appendChild(this.supplementInfoEl);
164
+ return;
165
+ }
146
166
  const files = this.props.getFiles();
147
- const file = files.find((f) => f.id === this.node.attrs.href);
167
+ const file = files.find((f) => f.id === href);
148
168
  if (file) {
149
169
  const iconElement = document.createElement('span');
150
170
  iconElement.classList.add('supplement-file-icon');
@@ -295,6 +295,32 @@ export const insertSupplement = (file, view, setSelection = true) => {
295
295
  view.dispatch(tr.scrollIntoView());
296
296
  return true;
297
297
  };
298
+ export const insertSupplementWeblink = (url, title, view) => {
299
+ const supplement = schema.nodes.supplement.createAndFill({
300
+ id: generateNodeID(schema.nodes.supplement),
301
+ href: url
302
+ }, createAndFillCaption());
303
+ const tr = view.state.tr;
304
+ const { pos } = upsertSupplementsSection(tr, supplement);
305
+ tr.setSelection(NodeSelection.create(tr.doc, pos));
306
+ view.focus();
307
+ view.dispatch(tr.scrollIntoView());
308
+ return true;
309
+ };
310
+ export const updateSupplementWeblink = (pos, url, title, view) => {
311
+ const node = view.state.doc.nodeAt(pos);
312
+ if (!node) {
313
+ return false;
314
+ }
315
+ const tr = view.state.tr;
316
+ tr.setNodeMarkup(pos, undefined, {
317
+ ...node.attrs,
318
+ href: url,
319
+ title: title,
320
+ });
321
+ view.dispatch(tr);
322
+ return true;
323
+ };
298
324
  export const insertAttachment = (file, state, type, dispatch) => {
299
325
  const tr = state.tr;
300
326
  const attachments = insertAttachmentsNode(tr);
@@ -74,4 +74,5 @@ export const openAuthorsAndAffiliationsModals = (pos, view, initialModal) => {
74
74
  const dialog = ReactSubView(props, AuthorsAndAffiliationsModals, componentProps, state.doc, () => pos, view);
75
75
  view.focus();
76
76
  document.body.appendChild(dialog);
77
+ return dialog;
77
78
  };
@@ -0,0 +1,142 @@
1
+ /*!
2
+ * © 2026 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import React, { useState } from 'react';
17
+ import { AttentionOrangeIcon, CloseButton, ModalContainer, ModalHeader, PrimaryButton, StyledModal, TertiaryButton, TextButton, } from '@manuscripts/style-guide';
18
+ import { startCase } from 'lodash';
19
+ import styled from 'styled-components';
20
+ export const CrossRefWarningModal = ({ onClose, xrefs, onConfirm, selectAndScrollTo }) => {
21
+ const [isOpen, setIsOpen] = useState(true);
22
+ const handleClose = () => {
23
+ setIsOpen(false);
24
+ onClose();
25
+ };
26
+ return (React.createElement(Modal, { isOpen: isOpen, onRequestClose: () => handleClose(), shouldCloseOnOverlayClick: false, hideOverlay: true },
27
+ React.createElement(Container, { "data-cy": "cross-reference-warning-modal" },
28
+ React.createElement(ModalHeader, null,
29
+ React.createElement(CloseButton, { onClick: () => handleClose(), "data-cy": "modal-close-button" })),
30
+ React.createElement(Body, null,
31
+ React.createElement(Title, null,
32
+ React.createElement(AttentionOrangeIcon, { width: 24, height: 22 }),
33
+ " Delete referenced content?"),
34
+ React.createElement("p", null, "You are deleting content referenced elsewhere in the document:"),
35
+ React.createElement(ScrolableItems, null, xrefs.map((group, i) => (React.createElement(XrefGroupDisplay, { key: i, group: group, selectAndScrollTo: selectAndScrollTo })))),
36
+ React.createElement(Actions, null,
37
+ React.createElement(TertiaryButton, { type: "button", onClick: () => handleClose() }, "Cancel"),
38
+ React.createElement(PrimaryButton, { "$danger": true, type: "button", onClick: () => {
39
+ onConfirm();
40
+ setIsOpen(false);
41
+ } }, "Delete & remove citation"))))));
42
+ };
43
+ const XrefGroupDisplay = ({ group, selectAndScrollTo }) => {
44
+ return (React.createElement("div", null,
45
+ React.createElement("h3", null, group.label),
46
+ React.createElement(ReferencesList, null, group.xrefs.map(([, pos], i) => {
47
+ return (React.createElement("li", { key: i },
48
+ React.createElement(TextButton, { onClick: () => selectAndScrollTo(pos) }, `${startCase(pos.parent.type.name)} - ${pos.parent.textContent}`)));
49
+ }))));
50
+ };
51
+ const Container = styled(ModalContainer) `
52
+ position: absolute;
53
+ top: 1rem;
54
+ left: 50%;
55
+ right: 0;
56
+ max-height: calc(50vh - 2rem);
57
+ min-height: 280px;
58
+ transform: translate(-50%, 0);
59
+ max-width: 480px;
60
+ transition:
61
+ top 0.2s,
62
+ transform 0.2s;
63
+ `;
64
+ const Modal = styled(StyledModal) `
65
+ position: fixed;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 100%;
70
+ z-index: 1100;
71
+ color: #6e6e6e;
72
+ margin: auto;
73
+
74
+ &.modal-bottom ${Container} {
75
+ top: calc(100% - 2rem);
76
+ transform: translate(-50%, -100%);
77
+ }
78
+
79
+ &:after {
80
+ content: '';
81
+ display: block;
82
+ position: fixed;
83
+ z-index: -1;
84
+ left: 0;
85
+ top: 0;
86
+ right: 0;
87
+ bottom: 0;
88
+ background: rgba(0, 0, 0, 0.2);
89
+ }
90
+ h3 {
91
+ font-size: 16px;
92
+ margin: 0.5em 0;
93
+ }
94
+ p {
95
+ margin: 0.5em 0;
96
+ }
97
+ `;
98
+ const Body = styled.div `
99
+ margin: 1.5rem;
100
+ display: flex;
101
+ flex-flow: column;
102
+ `;
103
+ const Title = styled.h2 `
104
+ font-size: 18px;
105
+ font-weight: 700;
106
+ line-height: 1.5;
107
+ margin: 0;
108
+ color: #353535;
109
+ svg {
110
+ vertical-align: text-top;
111
+ }
112
+ `;
113
+ const ReferencesList = styled.ul `
114
+ padding: 8px;
115
+ margin-left: 0;
116
+ list-style: none;
117
+ background: #f2f2f2;
118
+ border: 1px solid #e2e2e2;
119
+ border-radius: 3px;
120
+
121
+ ${TextButton} {
122
+ margin-left: 0;
123
+ text-decoration: underline;
124
+ &:hover {
125
+ text-decoration: none;
126
+ }
127
+ display: block;
128
+ max-width: 100%;
129
+ overflow: hidden;
130
+ color: #353535;
131
+ text-overflow: ellipsis;
132
+ }
133
+ `;
134
+ const Actions = styled.footer `
135
+ text-align: right;
136
+ padding-top: 1rem;
137
+ `;
138
+ const ScrolableItems = styled.div `
139
+ max-height: 16vh;
140
+ min-height: 100px;
141
+ overflow-y: auto;
142
+ `;
@@ -0,0 +1,31 @@
1
+ /*!
2
+ * © 2026 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { getEditorProps } from '../../plugins/editor-props';
17
+ import ReactSubView from '../../views/ReactSubView';
18
+ import { CrossRefWarningModal } from './CrossRefWarningModal';
19
+ export const openCrossRefWarningModal = (view, xrefGroups, onConfirm, onClose, selectAndScrollTo) => {
20
+ const { state } = view;
21
+ const props = getEditorProps(state);
22
+ const componentProps = {
23
+ xrefs: xrefGroups,
24
+ onConfirm,
25
+ onClose,
26
+ selectAndScrollTo,
27
+ };
28
+ const dialog = ReactSubView(props, CrossRefWarningModal, componentProps, state.doc, () => 0, view);
29
+ document.body.appendChild(dialog);
30
+ return dialog;
31
+ };
@@ -0,0 +1,35 @@
1
+ import { Category, Dialog, } from '@manuscripts/style-guide';
2
+ import React, { useState } from 'react';
3
+ import { getSupplementDisplayLabel, performDeleteSupplement, } from '../../lib/supplements';
4
+ import { getEditorProps } from '../../plugins/editor-props';
5
+ import ReactSubView from '../../views/ReactSubView';
6
+ const DeleteSupplementDialog = ({ label, onDelete }) => {
7
+ const [isOpen, setOpen] = useState(true);
8
+ return (React.createElement(Dialog, { isOpen: isOpen, category: Category.confirmation, header: 'Delete supplement', message: 'Are you sure you want to delete \u201C{label}\u201D?', actions: {
9
+ primary: {
10
+ action: () => {
11
+ onDelete();
12
+ setOpen(false);
13
+ },
14
+ title: 'Delete',
15
+ },
16
+ secondary: {
17
+ action: () => setOpen(false),
18
+ title: 'Cancel',
19
+ },
20
+ } }));
21
+ };
22
+ export const openDeleteSupplementDialog = (view, pos) => {
23
+ const node = view.state.doc.nodeAt(pos);
24
+ if (!node || node.type.name !== 'supplement') {
25
+ return;
26
+ }
27
+ const { state } = view;
28
+ const props = getEditorProps(state);
29
+ const label = getSupplementDisplayLabel(node, props.getFiles());
30
+ const dialog = ReactSubView(props, DeleteSupplementDialog, {
31
+ label,
32
+ onDelete: () => performDeleteSupplement(view, pos),
33
+ }, state.doc, () => 0, view);
34
+ document.body.appendChild(dialog);
35
+ };
package/dist/es/icons.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AddAuthorIcon, AlertIcon, ArrowDownCircleIcon, ArrowUpIcon, CameraIcon, DeleteIcon, DraggableIcon, EditIcon, FileCorruptedIcon, FileMainDocumentIcon, ImageDefaultIcon, ImageLeftIcon, ImageRightIcon, LinkIcon, LockIcon, LeadingHalfLeftIcon, LeadingHeroImageIcon, LeadingLargeFloatIcon, LeadingSmallFloatIcon, LeadingWallpaperIcon, ORCIDIcon as ORCID, PlusIcon, ScrollIcon, SectionCategoryIcon, TickIcon, TranslateIcon, } from '@manuscripts/style-guide';
1
+ import { AddAuthorIcon, AlertIcon, ArrowDownCircleIcon, ArrowUpIcon, CameraIcon, DeleteIcon, DraggableIcon, EditIcon, FileCorruptedIcon, FileMainDocumentIcon, ImageDefaultIcon, ImageLeftIcon, ImageRightIcon, LinkIcon, LockIcon, LeadingHalfLeftIcon, LeadingHeroImageIcon, LeadingLargeFloatIcon, LeadingSmallFloatIcon, LeadingWallpaperIcon, ORCIDIcon as ORCID, PlusIcon, ScrollIcon, SectionCategoryIcon, TickIcon, TranslateIcon, WebLinkIcon, } from '@manuscripts/style-guide';
2
2
  import { createElement } from 'react';
3
3
  import { renderToStaticMarkup } from 'react-dom/server';
4
4
  const renderIcon = (c) => renderToStaticMarkup(createElement(c));
@@ -28,3 +28,4 @@ export const leadingHalfLeftIcon = renderIcon(LeadingHalfLeftIcon);
28
28
  export const fileMainDocumentIcon = renderIcon(FileMainDocumentIcon);
29
29
  export const tickIcon = renderIcon(TickIcon);
30
30
  export const ORCIDIcon = renderIcon(ORCID);
31
+ export const webLinkIcon = renderIcon(WebLinkIcon);
package/dist/es/index.js CHANGED
@@ -29,6 +29,8 @@ export * from './lib/capabilities';
29
29
  export * from './lib/navigation-utils';
30
30
  export * from './lib/comments';
31
31
  export * from './lib/files';
32
+ export * from './lib/supplements';
33
+ export { allowedHref } from './lib/url';
32
34
  export * from './lib/footnotes';
33
35
  export * from './lib/template';
34
36
  export * from './lib/doc';
@@ -1,6 +1,7 @@
1
1
  import { schema } from '@manuscripts/transform';
2
2
  import { findChildrenByType } from 'prosemirror-utils';
3
3
  import { isHidden } from './track-changes-utils';
4
+ import { allowedHref } from './url';
4
5
  const MISSING_FILE = {
5
6
  id: '',
6
7
  name: '',
@@ -29,6 +30,7 @@ export const groupFiles = (doc, files) => {
29
30
  const fileMap = new Map(files.map((f) => [f.id, f]));
30
31
  const figures = [];
31
32
  const supplements = [];
33
+ const weblinks = [];
32
34
  const linkedFiles = [];
33
35
  const attachments = [];
34
36
  const getFile = (href) => {
@@ -91,11 +93,20 @@ export const groupFiles = (doc, files) => {
91
93
  }
92
94
  }
93
95
  if (node.type === schema.nodes.supplement) {
94
- supplements.push({
95
- node,
96
- pos,
97
- file: getFile(node.attrs.href),
98
- });
96
+ const href = node.attrs.href;
97
+ if (allowedHref(href)) {
98
+ weblinks.push({
99
+ node: node,
100
+ pos,
101
+ });
102
+ }
103
+ else {
104
+ supplements.push({
105
+ node,
106
+ pos,
107
+ file: getFile(href),
108
+ });
109
+ }
99
110
  }
100
111
  if (node.type === schema.nodes.attachment) {
101
112
  attachments.push({
@@ -108,6 +119,7 @@ export const groupFiles = (doc, files) => {
108
119
  return {
109
120
  figures,
110
121
  supplements,
122
+ weblinks,
111
123
  attachments,
112
124
  linkedFiles,
113
125
  others: [...fileMap.values()],
@@ -0,0 +1,55 @@
1
+ /*!
2
+ * © 2026 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { schema, } from '@manuscripts/transform';
17
+ import { findChildrenByType, findParentNodeClosestToPos } from 'prosemirror-utils';
18
+ import { allowedHref } from './url';
19
+ export const getSupplementDisplayLabel = (node, files) => {
20
+ const href = node.attrs.href;
21
+ if (allowedHref(href)) {
22
+ return href;
23
+ }
24
+ const file = files.find((f) => f.id === href);
25
+ return file?.name ?? 'Untitled supplement';
26
+ };
27
+ export const performDeleteSupplement = (view, pos) => {
28
+ const node = view.state.doc.nodeAt(pos);
29
+ if (!node || node.type !== schema.nodes.supplement) {
30
+ return false;
31
+ }
32
+ const from = pos;
33
+ const to = from + node.nodeSize;
34
+ const { from: deleteFrom, to: deleteTo } = deleteSupplementAtPos(view.state.doc, from, to);
35
+ view.dispatch(view.state.tr.delete(deleteFrom, deleteTo));
36
+ return true;
37
+ };
38
+ export const deleteSupplementAtPos = (doc, from, to) => {
39
+ const resolvedPos = doc.resolve(from);
40
+ const supplementsNodeWithPos = findParentNodeClosestToPos(resolvedPos, (node) => node.type === schema.nodes.supplements);
41
+ if (!supplementsNodeWithPos) {
42
+ return { from, to, deleteWholeSection: false };
43
+ }
44
+ const { node: supplementsNode, pos: supplementsPos } = supplementsNodeWithPos;
45
+ const supplements = findChildrenByType(supplementsNode, schema.nodes.supplement);
46
+ const isDeletingLastSupplement = supplements.length === 1;
47
+ if (isDeletingLastSupplement) {
48
+ return {
49
+ from: supplementsPos,
50
+ to: supplementsPos + supplementsNode.nodeSize,
51
+ deleteWholeSection: true,
52
+ };
53
+ }
54
+ return { from, to, deleteWholeSection: false };
55
+ };
@@ -13,13 +13,62 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { schema } from '@manuscripts/transform';
16
+ import { schema, } from '@manuscripts/transform';
17
+ import { trackChangesPluginKey } from '@manuscripts/track-changes-plugin';
17
18
  import { isEqual } from 'lodash';
18
- import { Plugin } from 'prosemirror-state';
19
+ import { NodeSelection, Plugin } from 'prosemirror-state';
19
20
  import { Decoration, DecorationSet } from 'prosemirror-view';
21
+ import { openCrossRefWarningModal } from '../components/cross-ref-check-modal/openModal';
20
22
  import { objectsKey } from './objects';
23
+ let modalActive = false;
24
+ let modalElement = null;
21
25
  export default () => {
26
+ let view = null;
22
27
  return new Plugin({
28
+ view(editorView) {
29
+ view = editorView;
30
+ return {
31
+ update(v) {
32
+ view = v;
33
+ },
34
+ destroy() {
35
+ view = null;
36
+ },
37
+ };
38
+ },
39
+ filterTransaction(tr, state) {
40
+ if (!view ||
41
+ !tr.docChanged ||
42
+ tr.getMeta(trackChangesPluginKey) ||
43
+ tr.getMeta('addToHistory') === false) {
44
+ return true;
45
+ }
46
+ if (modalActive) {
47
+ return false;
48
+ }
49
+ const targets = objectsKey.getState(state);
50
+ const xrefGroups = createXrefGroups(targets, state.doc, tr.doc);
51
+ if (xrefGroups.length === 0) {
52
+ return true;
53
+ }
54
+ modalActive = true;
55
+ const cleanup = () => {
56
+ modalActive = false;
57
+ if (modalElement) {
58
+ modalElement.classList.remove('modal-bottom');
59
+ modalElement.remove();
60
+ modalElement = null;
61
+ }
62
+ };
63
+ const deletedIds = new Set(xrefGroups.map((g) => g.referenced.attrs.id));
64
+ const onClose = () => {
65
+ cleanup();
66
+ };
67
+ if (view) {
68
+ modalElement = openCrossRefWarningModal(view, xrefGroups, onConfirmCreator(view, cleanup, deletedIds, tr), onClose, selectAndScrollToCreator(view));
69
+ }
70
+ return false;
71
+ },
23
72
  state: {
24
73
  init() {
25
74
  return DecorationSet.empty;
@@ -53,3 +102,114 @@ function createDecorations(doc) {
53
102
  });
54
103
  return decorations;
55
104
  }
105
+ function createXrefGroups(targets, stateDoc, resultDoc) {
106
+ const newIds = new Set();
107
+ const xrefRids = new Set();
108
+ const xrefGroups = [];
109
+ resultDoc.descendants((node) => {
110
+ if (node.attrs.id) {
111
+ newIds.add(node.attrs.id);
112
+ }
113
+ if (node.type === schema.nodes.cross_reference) {
114
+ for (const rid of node.attrs.rids) {
115
+ xrefRids.add(rid);
116
+ }
117
+ }
118
+ });
119
+ const orphanedRids = new Set();
120
+ for (const rid of xrefRids) {
121
+ if (!newIds.has(rid)) {
122
+ orphanedRids.add(rid);
123
+ }
124
+ }
125
+ if (orphanedRids.size === 0) {
126
+ return [];
127
+ }
128
+ const referencedNodes = new Map();
129
+ const xrefsByRid = new Map();
130
+ stateDoc.descendants((node, pos) => {
131
+ const id = node.attrs.id;
132
+ if (id && orphanedRids.has(id)) {
133
+ referencedNodes.set(id, node);
134
+ }
135
+ if (node.type === schema.nodes.cross_reference) {
136
+ for (const rid of node.attrs.rids) {
137
+ if (orphanedRids.has(rid)) {
138
+ let entries = xrefsByRid.get(rid);
139
+ if (!entries) {
140
+ entries = [];
141
+ xrefsByRid.set(rid, entries);
142
+ }
143
+ entries.push([node, stateDoc.resolve(pos)]);
144
+ }
145
+ }
146
+ }
147
+ });
148
+ for (const [id, referenced] of referencedNodes) {
149
+ const xrefs = xrefsByRid.get(id);
150
+ if (xrefs?.length) {
151
+ const label = targets.get(referenced.attrs.id)?.label || '';
152
+ xrefGroups.push({ referenced, label, xrefs });
153
+ }
154
+ }
155
+ return xrefGroups;
156
+ }
157
+ const onConfirmCreator = (view, cleanup, deletedIds, tr) => () => {
158
+ cleanup();
159
+ if (!view) {
160
+ return;
161
+ }
162
+ const newTr = view.state.tr;
163
+ for (const step of tr.steps) {
164
+ const result = step.apply(newTr.doc);
165
+ if (result.failed) {
166
+ console.warn('Cross-ref deletion warning: could not replay step —', result.failed);
167
+ return;
168
+ }
169
+ newTr.step(step);
170
+ }
171
+ const xrefPositions = [];
172
+ newTr.doc.descendants((node, pos) => {
173
+ if (node.type === schema.nodes.cross_reference) {
174
+ const rids = node.attrs.rids;
175
+ if (rids.some((rid) => deletedIds.has(rid))) {
176
+ xrefPositions.push({ from: pos, to: pos + node.nodeSize });
177
+ }
178
+ }
179
+ });
180
+ for (let i = xrefPositions.length - 1; i >= 0; i--) {
181
+ const { from, to } = xrefPositions[i];
182
+ newTr.delete(from, to);
183
+ }
184
+ view.dispatch(newTr);
185
+ };
186
+ const selectAndScrollToCreator = (view) => ($pos) => {
187
+ if (!view) {
188
+ return;
189
+ }
190
+ const selTr = view.state.tr;
191
+ selTr.setSelection(NodeSelection.create(view.state.doc, $pos.pos));
192
+ view.focus();
193
+ view.dispatch(selTr);
194
+ let scrollable = view.dom.parentElement;
195
+ while (scrollable != null &&
196
+ scrollable.scrollHeight <= scrollable.clientHeight) {
197
+ scrollable = scrollable.parentElement;
198
+ }
199
+ if (!scrollable) {
200
+ return;
201
+ }
202
+ const coords = view.coordsAtPos($pos.pos);
203
+ const containerRect = scrollable.getBoundingClientRect();
204
+ const offsetInContainer = coords.top - containerRect.top + scrollable.scrollTop;
205
+ const containerHeight = scrollable.clientHeight;
206
+ const scrollTo = offsetInContainer - containerHeight * 0.75;
207
+ if (scrollTo < 0) {
208
+ modalElement?.classList.add('modal-bottom');
209
+ scrollable.scrollTo({ top: 0, behavior: 'smooth' });
210
+ }
211
+ else {
212
+ modalElement?.classList.remove('modal-bottom');
213
+ scrollable.scrollTo({ top: scrollTo, behavior: 'smooth' });
214
+ }
215
+ };
@@ -1,2 +1,2 @@
1
- export const VERSION = '3.13.19';
1
+ export const VERSION = '3.15.0';
2
2
  export const MATHJAX_VERSION = '3.2.2';
@@ -20,6 +20,7 @@ export class CaptionTitleView extends BaseNodeView {
20
20
  super(...arguments);
21
21
  this.initialise = () => {
22
22
  this.createDOM();
23
+ this.updateContents();
23
24
  };
24
25
  }
25
26
  createDOM() {
@@ -15,8 +15,9 @@
15
15
  */
16
16
  import { getFileIcon } from '@manuscripts/style-guide';
17
17
  import { renderToStaticMarkup } from 'react-dom/server';
18
- import { draggableIcon } from '../icons';
18
+ import { draggableIcon, webLinkIcon } from '../icons';
19
19
  import { findNodeByID } from '../lib/doc';
20
+ import { allowedHref } from '../lib/url';
20
21
  import { BaseNodeView } from './base_node_view';
21
22
  import { createNodeView } from './creators';
22
23
  export class SupplementView extends BaseNodeView {
@@ -140,8 +141,27 @@ export class SupplementView extends BaseNodeView {
140
141
  this.supplementInfoEl = document.createElement('div');
141
142
  this.supplementInfoEl.classList.add('supplement-file-info');
142
143
  this.supplementInfoEl.contentEditable = 'false';
144
+ const href = this.node.attrs.href;
145
+ if (allowedHref(href)) {
146
+ const iconElement = document.createElement('span');
147
+ iconElement.classList.add('supplement-file-icon');
148
+ iconElement.innerHTML = webLinkIcon;
149
+ this.supplementInfoEl.appendChild(iconElement);
150
+ const urlLink = document.createElement('a');
151
+ urlLink.classList.add('supplement-weblink-url');
152
+ urlLink.textContent = href;
153
+ if (allowedHref(href)) {
154
+ urlLink.href = href;
155
+ urlLink.target = '_blank';
156
+ urlLink.rel = 'noopener noreferrer';
157
+ }
158
+ urlLink.addEventListener('mousedown', (e) => e.stopPropagation());
159
+ this.supplementInfoEl.appendChild(urlLink);
160
+ this.dom.appendChild(this.supplementInfoEl);
161
+ return;
162
+ }
143
163
  const files = this.props.getFiles();
144
- const file = files.find((f) => f.id === this.node.attrs.href);
164
+ const file = files.find((f) => f.id === href);
145
165
  if (file) {
146
166
  const iconElement = document.createElement('span');
147
167
  iconElement.classList.add('supplement-file-icon');
@@ -34,6 +34,8 @@ export declare const insertFigure: (file: FileAttachment, state: ManuscriptEdito
34
34
  export declare const insertEmbed: (state: ManuscriptEditorState, dispatch?: Dispatch, attrs?: Attrs) => boolean;
35
35
  export declare const insertTable: (config: TableConfig, state: ManuscriptEditorState, dispatch?: Dispatch) => boolean;
36
36
  export declare const insertSupplement: (file: FileAttachment, view: ManuscriptEditorView, setSelection?: boolean) => boolean;
37
+ export declare const insertSupplementWeblink: (url: string, title: string, view: ManuscriptEditorView) => boolean;
38
+ export declare const updateSupplementWeblink: (pos: number, url: string, title: string, view: ManuscriptEditorView) => boolean;
37
39
  export declare const insertAttachment: (file: FileAttachment, state: ManuscriptEditorState, type: string, dispatch?: Dispatch) => boolean;
38
40
  export declare const insertBlock: (nodeType: ManuscriptNodeType) => (state: ManuscriptEditorState, dispatch?: Dispatch) => boolean;
39
41
  export declare const deleteBlock: (typeToDelete: string) => (state: ManuscriptEditorState, dispatch?: Dispatch) => boolean;