@manuscripts/body-editor 3.12.66 → 3.13.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/dist/cjs/components/toolbar/type-selector/styles.js +2 -1
- package/dist/cjs/versions.js +1 -1
- package/dist/es/components/toolbar/type-selector/styles.js +2 -1
- package/dist/es/versions.js +1 -1
- package/dist/types/components/toolbar/type-selector/styles.d.ts +2 -2
- package/dist/types/lib/footnotes.d.ts +4 -2
- package/dist/types/lib/utils.d.ts +5 -4
- package/dist/types/versions.d.ts +1 -1
- package/package.json +37 -36
- package/{dist/es/lib/statistics.js → src/__mocks__/styleMock.ts} +2 -14
- package/src/assets.ts +20 -0
- package/src/classes/collabProvider.ts +38 -0
- package/src/clipboard.ts +54 -0
- package/src/commands.ts +2109 -0
- package/src/components/ChangeHandlingForm.tsx +54 -0
- package/src/components/LanguageDropdown/index.tsx +343 -0
- package/src/components/affiliations/AffiliationForm.tsx +238 -0
- package/src/components/affiliations/AffiliationItem.tsx +151 -0
- package/src/components/affiliations/AffiliationList.tsx +71 -0
- package/src/components/affiliations/AffiliationsModal.tsx +676 -0
- package/src/components/affiliations/AffiliationsPanel.tsx +76 -0
- package/src/components/affiliations/CreateAffiliationModal.tsx +135 -0
- package/src/components/authors/AuthorDetailsForm.tsx +438 -0
- package/src/components/authors/AuthorList.tsx +81 -0
- package/src/components/authors/AuthorsModal.tsx +642 -0
- package/src/components/authors/AuthorsPanel.tsx +69 -0
- package/src/components/authors/CreateAuthorModal.tsx +172 -0
- package/src/components/authors/CreditDrawer.tsx +71 -0
- package/src/components/authors/DraggableAuthor.tsx +228 -0
- package/src/components/authors/useManageAffiliations.tsx +77 -0
- package/src/components/authors/useManageCredit.tsx +63 -0
- package/src/components/authors-affiliations/AuthorsAndAffiliationsModals.tsx +170 -0
- package/src/components/authors-affiliations/GenericPanel.tsx +164 -0
- package/src/components/authors-affiliations/ModalTabs.tsx +100 -0
- package/src/components/awards/AwardForm.tsx +338 -0
- package/src/components/awards/AwardModal.tsx +120 -0
- package/src/components/awards/DeleteAwardDiaolog.tsx +52 -0
- package/src/components/dialog/ConfirmationDialog.tsx +131 -0
- package/src/components/form/CreateModalStyles.tsx +37 -0
- package/src/components/form/FormFooter.tsx +54 -0
- package/src/components/form/FormPlaceholder.tsx +71 -0
- package/src/components/form/ModalFormActions.tsx +88 -0
- package/src/components/form/UnsavedLabel.tsx +49 -0
- package/src/components/hooks/use-debounce.ts +56 -0
- package/src/components/hooks/useAffiliationShowsErrorIndicator.ts +51 -0
- package/src/components/hooks/useAuthorShowsErrorIndicator.ts +73 -0
- package/src/components/keyboard-shortcuts-modal/FormattedShortcut.tsx +56 -0
- package/src/components/keyboard-shortcuts-modal/KeyboardShortcutsModal.tsx +283 -0
- package/src/components/keyboard-shortcuts-modal/keyboard-shortcuts.ts +304 -0
- package/src/components/keywords/AddKeywordInline.tsx +284 -0
- package/src/components/keywords/DeleteKeywordDialog.tsx +52 -0
- package/src/components/modal-drawer/GenericDrawer.tsx +65 -0
- package/src/components/modal-drawer/GenericDrawerGroup.tsx +125 -0
- package/src/components/outline/DraggableTree.tsx +438 -0
- package/src/components/outline/ManuscriptOutline.tsx +127 -0
- package/src/components/outline/Outline.tsx +141 -0
- package/src/components/references/BibliographyItemSource.ts +80 -0
- package/src/components/references/CitationEditor.tsx +301 -0
- package/src/components/references/CitationViewer.tsx +62 -0
- package/src/components/references/ImportBibliographyForm.tsx +232 -0
- package/src/components/references/ImportBibliographyModal.tsx +159 -0
- package/src/components/references/ReferenceForm/PersonDropDown.tsx +93 -0
- package/src/components/references/ReferenceForm/ReferenceForm.tsx +701 -0
- package/src/components/references/ReferenceForm/config.ts +133 -0
- package/src/components/references/ReferenceForm/styled-components.ts +156 -0
- package/src/components/references/ReferenceLine.tsx +43 -0
- package/src/components/references/ReferenceSearch.tsx +163 -0
- package/src/components/references/ReferenceSearchResults.tsx +113 -0
- package/src/components/references/ReferenceSearchResultsPlaceholder.tsx +71 -0
- package/src/components/references/ReferenceSearchSection.tsx +124 -0
- package/src/components/references/ReferencesEditor.tsx +61 -0
- package/src/components/references/ReferencesModal.tsx +352 -0
- package/src/components/references/SearchInput.tsx +98 -0
- package/src/components/toolbar/InsertEmbedDialog.tsx +222 -0
- package/src/components/toolbar/InsertTableDialog.tsx +208 -0
- package/src/components/toolbar/ListMenuItem.tsx +203 -0
- package/src/components/toolbar/helpers.ts +547 -0
- package/src/components/toolbar/type-selector/OptionComponent.tsx +89 -0
- package/src/components/toolbar/type-selector/TypeSelector.tsx +157 -0
- package/src/components/toolbar/type-selector/styles.ts +79 -0
- package/src/components/views/CrossReferenceItems.tsx +199 -0
- package/src/components/views/DeleteEmbedDialog.tsx +83 -0
- package/src/components/views/DeleteFootnoteDialog.tsx +54 -0
- package/src/components/views/FigureDropdown.tsx +423 -0
- package/src/components/views/FootnotesSelector.tsx +255 -0
- package/src/components/views/InsertSpecialCharacter.tsx +245 -0
- package/src/components/views/LinkForm.tsx +220 -0
- package/src/components/views/TableCellContextMenu.tsx +241 -0
- package/src/configs/ManuscriptsEditor.ts +140 -0
- package/src/configs/editor-plugins.ts +119 -0
- package/src/configs/editor-views.ts +139 -0
- package/src/icons.ts +62 -0
- package/src/index.ts +65 -0
- package/src/keys/highlight.ts +65 -0
- package/src/keys/index.ts +33 -0
- package/src/keys/keyword.ts +69 -0
- package/src/keys/list.ts +156 -0
- package/src/keys/misc.ts +112 -0
- package/src/keys/title.ts +221 -0
- package/src/lib/__tests__/__fixtures__/doc.json +388 -0
- package/src/lib/__tests__/__snapshots__/paste.test.ts.snap +24 -0
- package/src/lib/__tests__/authors.test.ts +202 -0
- package/src/lib/__tests__/helpers.test.ts +46 -0
- package/src/lib/__tests__/paste.test.ts +57 -0
- package/src/lib/__tests__/plugins.test.ts +176 -0
- package/src/lib/__tests__/url.test.ts +31 -0
- package/src/lib/array-reducer.ts +68 -0
- package/src/lib/authors-and-affiliations.ts +81 -0
- package/src/lib/authors.ts +67 -0
- package/src/lib/capabilities.ts +55 -0
- package/src/lib/codemirror.ts +37 -0
- package/src/lib/comments.ts +123 -0
- package/src/lib/context-menu.ts +745 -0
- package/src/lib/copy.ts +30 -0
- package/src/lib/crossref.ts +90 -0
- package/src/lib/dnd.ts +29 -0
- package/src/lib/doc.ts +167 -0
- package/src/lib/dompurify.ts +28 -0
- package/src/lib/files.ts +180 -0
- package/src/lib/footnotes.ts +83 -0
- package/src/lib/get-media-type.ts +105 -0
- package/src/lib/helpers.ts +199 -0
- package/src/lib/hierarchy.ts +117 -0
- package/src/lib/languages.ts +37 -0
- package/src/lib/lists.ts +56 -0
- package/src/lib/math.ts +111 -0
- package/src/lib/media.ts +334 -0
- package/src/lib/navigation-utils.ts +235 -0
- package/src/lib/normalize.ts +70 -0
- package/src/lib/paste.ts +304 -0
- package/src/lib/placeholder.ts +32 -0
- package/src/lib/platform.ts +18 -0
- package/src/lib/plugins.ts +22 -0
- package/src/lib/popper.ts +180 -0
- package/src/lib/position-menu.ts +184 -0
- package/src/lib/references.ts +94 -0
- package/src/lib/template.ts +29 -0
- package/src/lib/track-changes-utils.ts +34 -0
- package/{dist/types/lib/statistics.d.ts → src/lib/url.ts} +11 -8
- package/src/lib/use-do-with-debounce.ts +39 -0
- package/src/lib/utils.ts +323 -0
- package/src/lib/view.ts +100 -0
- package/src/menus.tsx +684 -0
- package/src/node-type-icons.tsx +83 -0
- package/src/plugins/__tests__/__fixtures__/deleted-footnote.json +140 -0
- package/src/plugins/__tests__/__fixtures__/inserted-footnote.json +153 -0
- package/src/plugins/__tests__/footnotes.test.ts +67 -0
- package/src/plugins/accessibility_element.ts +219 -0
- package/src/plugins/add-subtitle.ts +102 -0
- package/src/plugins/affiliations.ts +136 -0
- package/src/plugins/alt-titles.ts +219 -0
- package/src/plugins/bibliography.ts +235 -0
- package/src/plugins/comments.ts +328 -0
- package/src/plugins/cross-references.ts +70 -0
- package/src/plugins/detect-inconsistency/detect-inconsistency-utils.ts +102 -0
- package/src/plugins/detect-inconsistency/index.ts +55 -0
- package/src/plugins/detect-inconsistency/validators.ts +283 -0
- package/src/plugins/doi.ts +66 -0
- package/src/plugins/editor-props.ts +39 -0
- package/src/plugins/elements.ts +60 -0
- package/src/plugins/footnotes.ts +298 -0
- package/src/plugins/link.ts +53 -0
- package/src/plugins/lock-body.ts +136 -0
- package/src/plugins/move-node.ts +55 -0
- package/src/plugins/objects.ts +125 -0
- package/src/plugins/paragraphs.ts +90 -0
- package/src/plugins/persist.ts +64 -0
- package/src/plugins/persistent-cursor.ts +66 -0
- package/src/plugins/placeholder.ts +219 -0
- package/src/plugins/prevent-empty.ts +50 -0
- package/src/plugins/search-replace/index.ts +157 -0
- package/src/plugins/search-replace/lib.ts +95 -0
- package/src/plugins/section_category.ts +212 -0
- package/src/plugins/section_title/autocompletion.ts +87 -0
- package/src/plugins/section_title/index.ts +105 -0
- package/src/plugins/selected-suggestion.ts +243 -0
- package/src/plugins/tables-cursor-fix.ts +143 -0
- package/src/plugins/translations.ts +263 -0
- package/src/rules.ts +60 -0
- package/src/selection.ts +112 -0
- package/src/testing/default-capabilities.ts +48 -0
- package/src/testing/default-editor-data.ts +73 -0
- package/src/testing/empty-editor-doc.json +62 -0
- package/src/testing/polyfill-dom.ts +43 -0
- package/src/testing/section-categories.json +161 -0
- package/src/testing/setup-editor.ts +55 -0
- package/src/tests.ts +42 -0
- package/src/toolbar.tsx +259 -0
- package/src/types/@citation-js/core.d.ts +16 -0
- package/src/types/@citation-js/index.d.ts +16 -0
- package/src/types/@citation-js/plugin-bibtex.d.ts +16 -0
- package/src/types/@citation-js/plugin-csl.d.ts +16 -0
- package/src/types/@citation-js/plugin-doi.d.ts +16 -0
- package/src/types/@citation-js/plugin-enw.d.ts +16 -0
- package/src/types/@citation-js/plugin-pubmed.d.ts +16 -0
- package/src/types/@citation-js/plugin-ris.d.ts +16 -0
- package/src/types/citeproc.d.ts +124 -0
- package/src/types/css.d.ts +16 -0
- package/src/types/json.d.ts +17 -0
- package/src/types/prosemirror-dev-tools.d.ts +16 -0
- package/src/types/prosemirror-dropcursor.d.ts +16 -0
- package/src/types/styled-components.d.ts +23 -0
- package/src/types.ts +49 -0
- package/src/useEditor.ts +244 -0
- package/src/versions.ts +3 -0
- package/src/views/ReactSubView.tsx +145 -0
- package/src/views/abstracts.ts +46 -0
- package/src/views/accessibility_element.ts +86 -0
- package/src/views/affiliations.ts +254 -0
- package/src/views/alt_title.ts +85 -0
- package/src/views/alt_titles_section.ts +97 -0
- package/src/views/attachment.ts +160 -0
- package/src/views/attachments.ts +72 -0
- package/src/views/author_notes.ts +96 -0
- package/src/views/award.ts +194 -0
- package/src/views/awards.ts +59 -0
- package/src/views/base_node_view.ts +85 -0
- package/src/views/bibliography_element.ts +313 -0
- package/src/views/block_view.ts +100 -0
- package/src/views/blockquote_element.ts +26 -0
- package/src/views/blockquote_element_editable.ts +24 -0
- package/src/views/box_element.ts +33 -0
- package/src/views/caption.ts +37 -0
- package/src/views/caption_title.ts +33 -0
- package/src/views/citation.ts +67 -0
- package/src/views/citation_editable.ts +312 -0
- package/src/views/contributors.ts +341 -0
- package/src/views/creators.ts +82 -0
- package/src/views/cross_reference.ts +74 -0
- package/src/views/cross_reference_editable.ts +113 -0
- package/src/views/editable_block.ts +141 -0
- package/src/views/embed.ts +345 -0
- package/src/views/empty.ts +26 -0
- package/src/views/equation.ts +47 -0
- package/src/views/equation_editable.ts +84 -0
- package/src/views/equation_element.ts +29 -0
- package/src/views/equation_element_editable.ts +21 -0
- package/src/views/figure.ts +84 -0
- package/src/views/figure_editable.ts +361 -0
- package/src/views/figure_element.ts +197 -0
- package/src/views/figure_element_editable.ts +21 -0
- package/src/views/footnote.ts +236 -0
- package/src/views/footnotes_element.ts +54 -0
- package/src/views/general_table_footnote.ts +140 -0
- package/src/views/hero_image.ts +76 -0
- package/src/views/hero_image_editable.ts +21 -0
- package/src/views/image_element.ts +373 -0
- package/src/views/image_element_editable.ts +21 -0
- package/src/views/inline_equation.ts +47 -0
- package/src/views/inline_equation_editable.ts +70 -0
- package/src/views/inline_footnote.ts +264 -0
- package/src/views/inline_footnote_editable.ts +20 -0
- package/src/views/keyword.ts +127 -0
- package/src/views/keyword_group.ts +109 -0
- package/src/views/link.ts +27 -0
- package/src/views/link_editable.ts +169 -0
- package/src/views/list.ts +46 -0
- package/src/views/list_item.ts +26 -0
- package/src/views/paragraph.ts +26 -0
- package/src/views/paragraph_editable.ts +21 -0
- package/src/views/placeholder.ts +35 -0
- package/src/views/placeholder_element.ts +38 -0
- package/src/views/placeholder_element_editable.ts +21 -0
- package/src/views/pullquote_element.ts +106 -0
- package/src/views/pullquote_element_editable.ts +24 -0
- package/src/views/quote_image_editable.ts +84 -0
- package/src/views/section.ts +64 -0
- package/src/views/section_label.ts +20 -0
- package/src/views/section_title.ts +87 -0
- package/src/views/section_title_editable.ts +21 -0
- package/src/views/subtitle.ts +42 -0
- package/src/views/subtitle_editable.ts +20 -0
- package/src/views/subtitles.ts +34 -0
- package/src/views/subtitles_editable.ts +21 -0
- package/src/views/supplement.ts +236 -0
- package/src/views/supplement_editable.ts +20 -0
- package/src/views/supplements.ts +65 -0
- package/src/views/supplements_editable.ts +21 -0
- package/src/views/table_cell.ts +150 -0
- package/src/views/table_element.ts +33 -0
- package/src/views/table_element_editable.ts +21 -0
- package/src/views/table_element_footer.ts +41 -0
- package/src/views/title.ts +46 -0
- package/src/views/title_editable.ts +20 -0
- package/src/views/trans_graphical_abstract.ts +55 -0
- package/src/views/trans_graphical_abstract_editable.ts +21 -0
- package/src/views/translated_abstract.ts +53 -0
- package/src/views/translated_abstract_editable.ts +21 -0
- package/dist/cjs/lib/statistics.js +0 -38
package/src/commands.ts
ADDED
|
@@ -0,0 +1,2109 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* © 2019 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
|
+
|
|
17
|
+
import { isDeleted, skipTracking, skipSelect } from '@manuscripts/track-changes-plugin'
|
|
18
|
+
import {
|
|
19
|
+
AttachmentNode,
|
|
20
|
+
AwardNode,
|
|
21
|
+
BoxElementNode,
|
|
22
|
+
FigureElementNode,
|
|
23
|
+
FigureNode,
|
|
24
|
+
FootnotesElementNode,
|
|
25
|
+
generateNodeID,
|
|
26
|
+
GraphicalAbstractSectionNode,
|
|
27
|
+
isElementNodeType,
|
|
28
|
+
isListNode,
|
|
29
|
+
isSectionNodeType,
|
|
30
|
+
isTableElementNode,
|
|
31
|
+
KeywordsNode,
|
|
32
|
+
ListNode,
|
|
33
|
+
ManuscriptEditorState,
|
|
34
|
+
ManuscriptEditorView,
|
|
35
|
+
ManuscriptMarkType,
|
|
36
|
+
ManuscriptNode,
|
|
37
|
+
ManuscriptNodeSelection,
|
|
38
|
+
ManuscriptNodeType,
|
|
39
|
+
ManuscriptResolvedPos,
|
|
40
|
+
ManuscriptTextSelection,
|
|
41
|
+
ManuscriptTransaction,
|
|
42
|
+
schema,
|
|
43
|
+
SectionCategory,
|
|
44
|
+
SectionNode,
|
|
45
|
+
SupplementNode,
|
|
46
|
+
TransGraphicalAbstractNode,
|
|
47
|
+
} from '@manuscripts/transform'
|
|
48
|
+
import {
|
|
49
|
+
Attrs,
|
|
50
|
+
Fragment,
|
|
51
|
+
NodeRange,
|
|
52
|
+
ResolvedPos,
|
|
53
|
+
Slice,
|
|
54
|
+
} from 'prosemirror-model'
|
|
55
|
+
import { wrapInList } from 'prosemirror-schema-list'
|
|
56
|
+
import {
|
|
57
|
+
EditorState,
|
|
58
|
+
NodeSelection,
|
|
59
|
+
Selection,
|
|
60
|
+
TextSelection,
|
|
61
|
+
Transaction,
|
|
62
|
+
} from 'prosemirror-state'
|
|
63
|
+
import {
|
|
64
|
+
addColumnAfter,
|
|
65
|
+
addColumnBefore,
|
|
66
|
+
addRow,
|
|
67
|
+
selectedRect,
|
|
68
|
+
} from 'prosemirror-tables'
|
|
69
|
+
import {
|
|
70
|
+
findWrapping,
|
|
71
|
+
ReplaceAroundStep,
|
|
72
|
+
ReplaceStep,
|
|
73
|
+
} from 'prosemirror-transform'
|
|
74
|
+
import {
|
|
75
|
+
findChildrenByType,
|
|
76
|
+
findParentNodeOfType,
|
|
77
|
+
findParentNodeOfTypeClosestToPos,
|
|
78
|
+
flatten,
|
|
79
|
+
hasParentNodeOfType,
|
|
80
|
+
NodeWithPos,
|
|
81
|
+
} from 'prosemirror-utils'
|
|
82
|
+
import { EditorView } from 'prosemirror-view'
|
|
83
|
+
|
|
84
|
+
import { CommentAttrs, getCommentKey, getCommentRange } from './lib/comments'
|
|
85
|
+
import {
|
|
86
|
+
findAbstractsNode,
|
|
87
|
+
findBackmatter,
|
|
88
|
+
findBibliographySection,
|
|
89
|
+
findBody,
|
|
90
|
+
findFootnotesSection,
|
|
91
|
+
insertAttachmentsNode,
|
|
92
|
+
insertAwardsNode,
|
|
93
|
+
insertFootnotesSection,
|
|
94
|
+
upsertSupplementsSection,
|
|
95
|
+
} from './lib/doc'
|
|
96
|
+
import { FileAttachment } from './lib/files'
|
|
97
|
+
import {
|
|
98
|
+
createFootnote,
|
|
99
|
+
findFootnotesContainerNode,
|
|
100
|
+
getFootnotesElementState,
|
|
101
|
+
} from './lib/footnotes'
|
|
102
|
+
import {
|
|
103
|
+
findWordBoundaries,
|
|
104
|
+
isNodeOfType,
|
|
105
|
+
nearestAncestor,
|
|
106
|
+
} from './lib/helpers'
|
|
107
|
+
import { templateAllows } from './lib/template'
|
|
108
|
+
import {
|
|
109
|
+
findInsertionPosition,
|
|
110
|
+
findParentNodeWithId,
|
|
111
|
+
getChildOfType,
|
|
112
|
+
getInsertPos,
|
|
113
|
+
isBodyLocked,
|
|
114
|
+
} from './lib/utils'
|
|
115
|
+
import { expandAccessibilitySection } from './plugins/accessibility_element'
|
|
116
|
+
import { setCommentSelection } from './plugins/comments'
|
|
117
|
+
import { getEditorProps } from './plugins/editor-props'
|
|
118
|
+
import { searchReplaceKey } from './plugins/search-replace'
|
|
119
|
+
import { checkForCompletion } from './plugins/section_title/autocompletion'
|
|
120
|
+
import { EditorAction } from './types'
|
|
121
|
+
import { persistentCursor } from './plugins/persistent-cursor'
|
|
122
|
+
import { AwardAttrs } from './views/award'
|
|
123
|
+
import { openAuthorsAndAffiliationsModals } from './components/authors-affiliations/AuthorsAndAffiliationsModals'
|
|
124
|
+
|
|
125
|
+
export type Dispatch = (tr: ManuscriptTransaction) => void
|
|
126
|
+
|
|
127
|
+
// enter at the start of paragraph will add node above
|
|
128
|
+
export const addToStart = (
|
|
129
|
+
state: ManuscriptEditorState,
|
|
130
|
+
dispatch?: Dispatch
|
|
131
|
+
): boolean => {
|
|
132
|
+
const { selection } = state
|
|
133
|
+
|
|
134
|
+
const props = getEditorProps(state)
|
|
135
|
+
if (props.getCapabilities().editWithoutTracking) {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
!dispatch ||
|
|
141
|
+
!(selection instanceof TextSelection) ||
|
|
142
|
+
(selection.$from.node().type !== schema.nodes.paragraph &&
|
|
143
|
+
selection.$from.node().type !== schema.nodes.text_block)
|
|
144
|
+
) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const {
|
|
149
|
+
$anchor: { parentOffset: startOffset },
|
|
150
|
+
$head: { parentOffset: endOffset },
|
|
151
|
+
$from,
|
|
152
|
+
$to,
|
|
153
|
+
} = selection
|
|
154
|
+
|
|
155
|
+
// Check if the cursor is at the start of a paragraph or text block.
|
|
156
|
+
// This ensures that a new node is added above the current node only when
|
|
157
|
+
// the cursor is at the very beginning of the parent node.
|
|
158
|
+
if (startOffset === 0 && endOffset === 0) {
|
|
159
|
+
const side =
|
|
160
|
+
(!$from.parentOffset && $to.index() < $to.parent.childCount ? $from : $to)
|
|
161
|
+
.pos - (startOffset === 0 ? 1 : 0)
|
|
162
|
+
|
|
163
|
+
const tr = state.tr
|
|
164
|
+
const from = $from.node().type.createAndFill()
|
|
165
|
+
if (from) {
|
|
166
|
+
tr.insert(side, from)
|
|
167
|
+
dispatch(tr.scrollIntoView())
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const markActive =
|
|
175
|
+
(type: ManuscriptMarkType) =>
|
|
176
|
+
(state: ManuscriptEditorState): boolean => {
|
|
177
|
+
const { from, $from, to, empty } = state.selection
|
|
178
|
+
|
|
179
|
+
let hasMark = false
|
|
180
|
+
|
|
181
|
+
state.doc.nodesBetween(from, to, (node) => {
|
|
182
|
+
if (node.isText) {
|
|
183
|
+
node.marks.forEach((m) => {
|
|
184
|
+
if (m.type === type && !isDeleted(m)) {
|
|
185
|
+
hasMark = true
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return empty
|
|
192
|
+
? Boolean(type.isInSet(state.storedMarks || $from.marks()))
|
|
193
|
+
: state.doc.rangeHasMark(from, to, type) && hasMark
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const isNodeSelection = (
|
|
197
|
+
selection: Selection
|
|
198
|
+
): selection is ManuscriptNodeSelection => selection instanceof NodeSelection
|
|
199
|
+
|
|
200
|
+
export const blockActive =
|
|
201
|
+
(type: ManuscriptNodeType) => (state: ManuscriptEditorState) => {
|
|
202
|
+
const { selection } = state
|
|
203
|
+
|
|
204
|
+
if (isNodeSelection(selection)) {
|
|
205
|
+
return selection.node.type === type
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { to, $from } = selection as ManuscriptTextSelection
|
|
209
|
+
|
|
210
|
+
if (to > $from.end()) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (let d = $from.depth; d >= 0; d--) {
|
|
215
|
+
const ancestor = $from.node(d)
|
|
216
|
+
|
|
217
|
+
// only look at the closest parent with an id
|
|
218
|
+
if (ancestor.attrs.id) {
|
|
219
|
+
return ancestor.type === type
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const canInsert =
|
|
227
|
+
(type: ManuscriptNodeType) => (state: ManuscriptEditorState) => {
|
|
228
|
+
const { $from, $to } = state.selection
|
|
229
|
+
|
|
230
|
+
if (!templateAllows(state, type)) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// disable block comment insertion just for title node, LEAN-2746
|
|
235
|
+
if (
|
|
236
|
+
($from.node().type === schema.nodes.title ||
|
|
237
|
+
$from.node().type === schema.nodes.section_title) &&
|
|
238
|
+
$from.pos === $to.pos &&
|
|
239
|
+
type !== schema.nodes.text
|
|
240
|
+
) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
isElementNodeType(type) &&
|
|
246
|
+
type !== schema.nodes.paragraph &&
|
|
247
|
+
findParentNodeOfType(schema.nodes.backmatter)(state.selection)
|
|
248
|
+
) {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (type === schema.nodes.table_element) {
|
|
253
|
+
if (findParentNodeOfType(schema.nodes.list_item)(state.selection)) {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Prevent inserting a box_element if:
|
|
259
|
+
// it's not inside a body node (invalid context), OR
|
|
260
|
+
// it's already nested inside another box_element (disallowed nesting)
|
|
261
|
+
if (
|
|
262
|
+
type === schema.nodes.box_element &&
|
|
263
|
+
(!hasParentNodeOfType(schema.nodes.body)(state.selection) ||
|
|
264
|
+
hasParentNodeOfType(schema.nodes.box_element)(state.selection))
|
|
265
|
+
) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
// Prevent inserting a hero_image if it already exists
|
|
269
|
+
if (
|
|
270
|
+
type === schema.nodes.hero_image &&
|
|
271
|
+
getChildOfType(state.doc, schema.nodes.hero_image, true)
|
|
272
|
+
) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const initDepth =
|
|
277
|
+
findParentNodeOfType(schema.nodes.box_element)(state.selection)?.depth ||
|
|
278
|
+
0
|
|
279
|
+
|
|
280
|
+
for (let d = $from.depth; d >= initDepth; d--) {
|
|
281
|
+
const index = $from.index(d)
|
|
282
|
+
|
|
283
|
+
if ($from.node(d).canReplaceWith(index, index, type)) {
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const findBlockInsertPosition = (state: ManuscriptEditorState) => {
|
|
292
|
+
const { $from } = state.selection
|
|
293
|
+
|
|
294
|
+
for (let d = $from.depth; d >= 0; d--) {
|
|
295
|
+
const node = $from.node(d)
|
|
296
|
+
|
|
297
|
+
if (isElementNodeType(node.type)) {
|
|
298
|
+
return $from.after(d)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const createSelection = (
|
|
306
|
+
nodeType: ManuscriptNodeType,
|
|
307
|
+
position: number,
|
|
308
|
+
doc: ManuscriptNode
|
|
309
|
+
) => {
|
|
310
|
+
const { nodes } = nodeType.schema
|
|
311
|
+
|
|
312
|
+
switch (nodeType) {
|
|
313
|
+
case nodes.figure_element:
|
|
314
|
+
// select the figure caption
|
|
315
|
+
return TextSelection.near(doc.resolve(position + 5), 1)
|
|
316
|
+
|
|
317
|
+
case nodes.listing_element:
|
|
318
|
+
// select the listing
|
|
319
|
+
return NodeSelection.create(doc, position + 1)
|
|
320
|
+
|
|
321
|
+
default:
|
|
322
|
+
return nodeType.isAtom
|
|
323
|
+
? NodeSelection.create(doc, position)
|
|
324
|
+
: TextSelection.near(doc.resolve(position + 1))
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const createBlock = (
|
|
329
|
+
nodeType: ManuscriptNodeType,
|
|
330
|
+
position: number,
|
|
331
|
+
state: ManuscriptEditorState,
|
|
332
|
+
dispatch?: Dispatch,
|
|
333
|
+
attrs?: Attrs
|
|
334
|
+
) => {
|
|
335
|
+
let node: ManuscriptNode
|
|
336
|
+
switch (nodeType) {
|
|
337
|
+
case schema.nodes.table_element:
|
|
338
|
+
node = createAndFillTableElement(attrs)
|
|
339
|
+
break
|
|
340
|
+
case schema.nodes.figure_element:
|
|
341
|
+
node = createAndFillFigureElement(attrs)
|
|
342
|
+
break
|
|
343
|
+
case schema.nodes.image_element:
|
|
344
|
+
node = createImageElement(attrs)
|
|
345
|
+
break
|
|
346
|
+
case schema.nodes.hero_image:
|
|
347
|
+
node = createHeroImage(attrs)
|
|
348
|
+
break
|
|
349
|
+
case schema.nodes.listing_element:
|
|
350
|
+
node = schema.nodes.listing_element.create({}, [
|
|
351
|
+
schema.nodes.listing.create(),
|
|
352
|
+
...createAndFillCaption(),
|
|
353
|
+
])
|
|
354
|
+
break
|
|
355
|
+
case schema.nodes.equation_element:
|
|
356
|
+
node = schema.nodes.equation_element.create({}, [
|
|
357
|
+
schema.nodes.equation.create(),
|
|
358
|
+
])
|
|
359
|
+
break
|
|
360
|
+
case schema.nodes.embed:
|
|
361
|
+
node = createEmbedElement(attrs)
|
|
362
|
+
break
|
|
363
|
+
case state.schema.nodes.pullquote_element:
|
|
364
|
+
node = state.schema.nodes.pullquote_element.create(attrs, [
|
|
365
|
+
state.schema.nodes.text_block.create({}),
|
|
366
|
+
state.schema.nodes.attribution.create({}),
|
|
367
|
+
])
|
|
368
|
+
break
|
|
369
|
+
default:
|
|
370
|
+
node = nodeType.createAndFill(attrs) as ManuscriptNode
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const tr = state.tr.insert(position, node)
|
|
374
|
+
|
|
375
|
+
if (dispatch) {
|
|
376
|
+
const selection = createSelection(nodeType, position, tr.doc)
|
|
377
|
+
expandAccessibilitySection(tr, node)
|
|
378
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const insertInlineTableFootnote = (
|
|
383
|
+
state: ManuscriptEditorState,
|
|
384
|
+
dispatch?: Dispatch
|
|
385
|
+
) => {
|
|
386
|
+
const $pos = state.selection.$to
|
|
387
|
+
const table = findParentNodeOfTypeClosestToPos($pos, schema.nodes.table)
|
|
388
|
+
if (!table) {
|
|
389
|
+
return false
|
|
390
|
+
}
|
|
391
|
+
if (!dispatch) {
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
return insertInlineFootnote(state, dispatch)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const insertGeneralTableFootnote = (
|
|
398
|
+
element: [ManuscriptNode, number],
|
|
399
|
+
state: ManuscriptEditorState,
|
|
400
|
+
dispatch?: Dispatch
|
|
401
|
+
) => {
|
|
402
|
+
const existing = findChildrenByType(
|
|
403
|
+
element[0],
|
|
404
|
+
schema.nodes.general_table_footnote
|
|
405
|
+
)
|
|
406
|
+
if (existing.length) {
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
if (!dispatch) {
|
|
410
|
+
return true
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const tr = state.tr
|
|
414
|
+
const footer = insertTableElementFooter(tr, element)
|
|
415
|
+
|
|
416
|
+
const pos = footer.pos + 2
|
|
417
|
+
const node = schema.nodes.general_table_footnote.create({}, [
|
|
418
|
+
schema.nodes.paragraph.create(),
|
|
419
|
+
])
|
|
420
|
+
tr.insert(pos, node)
|
|
421
|
+
const selection = TextSelection.create(tr.doc, pos + 1)
|
|
422
|
+
tr.setSelection(selection).scrollIntoView()
|
|
423
|
+
dispatch(tr)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export const insertFigure = (
|
|
427
|
+
file: FileAttachment,
|
|
428
|
+
state: ManuscriptEditorState,
|
|
429
|
+
dispatch?: Dispatch
|
|
430
|
+
) => {
|
|
431
|
+
const position = findBlockInsertPosition(state)
|
|
432
|
+
if (position === null || !dispatch) {
|
|
433
|
+
return false
|
|
434
|
+
}
|
|
435
|
+
const figure = state.schema.nodes.figure.createAndFill({
|
|
436
|
+
label: file.name,
|
|
437
|
+
src: file.id,
|
|
438
|
+
}) as FigureNode
|
|
439
|
+
|
|
440
|
+
const element = state.schema.nodes.figure_element.createAndFill({}, [
|
|
441
|
+
figure,
|
|
442
|
+
schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
|
|
443
|
+
]) as FigureElementNode
|
|
444
|
+
const tr = state.tr.insert(position, element)
|
|
445
|
+
dispatch(tr)
|
|
446
|
+
return true
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const insertEmbed = (
|
|
450
|
+
state: ManuscriptEditorState,
|
|
451
|
+
dispatch?: Dispatch,
|
|
452
|
+
attrs?: Attrs
|
|
453
|
+
) => {
|
|
454
|
+
const position = findBlockInsertPosition(state)
|
|
455
|
+
if (position === null) {
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const embed = schema.nodes.embed.create(
|
|
460
|
+
{
|
|
461
|
+
...attrs,
|
|
462
|
+
id: generateNodeID(schema.nodes.embed),
|
|
463
|
+
},
|
|
464
|
+
[
|
|
465
|
+
...createAndFillCaption(),
|
|
466
|
+
schema.nodes.alt_text.create(),
|
|
467
|
+
schema.nodes.long_desc.create(),
|
|
468
|
+
]
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
const tr = state.tr.insert(position, embed)
|
|
472
|
+
dispatch && dispatch(tr)
|
|
473
|
+
|
|
474
|
+
return true
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const insertTable = (
|
|
478
|
+
config: TableConfig,
|
|
479
|
+
state: ManuscriptEditorState,
|
|
480
|
+
dispatch?: Dispatch
|
|
481
|
+
) => {
|
|
482
|
+
const pos = findBlockInsertPosition(state)
|
|
483
|
+
if (!pos) {
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
const node = createAndFillTableElement(undefined, config)
|
|
487
|
+
const tr = state.tr.insert(pos, node)
|
|
488
|
+
expandAccessibilitySection(tr, node)
|
|
489
|
+
tr.setSelection(NodeSelection.create(tr.doc, pos)).scrollIntoView()
|
|
490
|
+
dispatch && dispatch(tr)
|
|
491
|
+
return true
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export const insertSupplement = (
|
|
495
|
+
file: FileAttachment,
|
|
496
|
+
view: ManuscriptEditorView,
|
|
497
|
+
setSelection = true
|
|
498
|
+
) => {
|
|
499
|
+
const supplement = schema.nodes.supplement.createAndFill(
|
|
500
|
+
{
|
|
501
|
+
id: generateNodeID(schema.nodes.supplement),
|
|
502
|
+
href: file.id,
|
|
503
|
+
},
|
|
504
|
+
createAndFillCaption()
|
|
505
|
+
) as SupplementNode
|
|
506
|
+
|
|
507
|
+
let tr = view.state.tr
|
|
508
|
+
const { pos } = upsertSupplementsSection(tr, supplement)
|
|
509
|
+
|
|
510
|
+
if (setSelection) {
|
|
511
|
+
tr.setSelection(NodeSelection.create(tr.doc, pos))
|
|
512
|
+
} else {
|
|
513
|
+
tr = skipSelect(tr)
|
|
514
|
+
}
|
|
515
|
+
view.focus()
|
|
516
|
+
view.dispatch(tr.scrollIntoView())
|
|
517
|
+
return true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export const insertAttachment = (
|
|
521
|
+
file: FileAttachment,
|
|
522
|
+
state: ManuscriptEditorState,
|
|
523
|
+
type: string,
|
|
524
|
+
dispatch?: Dispatch
|
|
525
|
+
) => {
|
|
526
|
+
const tr = state.tr
|
|
527
|
+
const attachments = insertAttachmentsNode(tr)
|
|
528
|
+
if (!attachments) {
|
|
529
|
+
return false
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (attachments.node.childCount > 0) {
|
|
533
|
+
const startPos = attachments.pos + 1
|
|
534
|
+
const endPos = attachments.pos + attachments.node.nodeSize - 1
|
|
535
|
+
tr.delete(startPos, endPos)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const attachment = schema.nodes.attachment.createAndFill({
|
|
539
|
+
id: generateNodeID(schema.nodes.attachment),
|
|
540
|
+
href: file.id,
|
|
541
|
+
type: type,
|
|
542
|
+
}) as AttachmentNode
|
|
543
|
+
|
|
544
|
+
tr.insert(attachments.pos + 1, attachment)
|
|
545
|
+
|
|
546
|
+
if (dispatch) {
|
|
547
|
+
dispatch(skipTracking(tr))
|
|
548
|
+
}
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export const insertBlock =
|
|
553
|
+
(nodeType: ManuscriptNodeType) =>
|
|
554
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch) => {
|
|
555
|
+
if (!canInsert(nodeType)(state)) {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
const position = findBlockInsertPosition(state)
|
|
559
|
+
if (position === null) {
|
|
560
|
+
return false
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
createBlock(nodeType, position, state, dispatch, undefined)
|
|
564
|
+
|
|
565
|
+
return true
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export const deleteBlock =
|
|
569
|
+
(typeToDelete: string) =>
|
|
570
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch) => {
|
|
571
|
+
const { selection, tr } = state
|
|
572
|
+
const { $head } = selection
|
|
573
|
+
const depth = nearestAncestor(isNodeOfType(typeToDelete))($head)
|
|
574
|
+
|
|
575
|
+
if (!depth) {
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (dispatch) {
|
|
580
|
+
const start = $head.start(depth)
|
|
581
|
+
const end = $head.end(depth)
|
|
582
|
+
tr.delete(start - 1, end + 1)
|
|
583
|
+
dispatch(tr)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return true
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export const insertBreak: EditorAction = (state, dispatch) => {
|
|
590
|
+
const br = state.schema.nodes.hard_break.create()
|
|
591
|
+
|
|
592
|
+
const tr = state.tr.replaceSelectionWith(br)
|
|
593
|
+
|
|
594
|
+
if (dispatch) {
|
|
595
|
+
dispatch(tr.scrollIntoView())
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return true
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const selectedText = (): string => (window.getSelection() || '').toString()
|
|
602
|
+
|
|
603
|
+
export const findPosBeforeFirstSubsection = (
|
|
604
|
+
$pos: ManuscriptResolvedPos
|
|
605
|
+
): number | null => {
|
|
606
|
+
let posBeforeFirstSubsection: number | null = null
|
|
607
|
+
|
|
608
|
+
for (let d = $pos.depth; d >= 0; d--) {
|
|
609
|
+
const parentNode = $pos.node(d)
|
|
610
|
+
if (isSectionNodeType(parentNode.type)) {
|
|
611
|
+
const parentStartPos = $pos.start(d) // Get the start position of the parent section
|
|
612
|
+
parentNode.descendants((node, pos, parent) => {
|
|
613
|
+
// Only consider direct children of the section
|
|
614
|
+
if (
|
|
615
|
+
node.type === schema.nodes.section &&
|
|
616
|
+
parent === parentNode &&
|
|
617
|
+
posBeforeFirstSubsection === null
|
|
618
|
+
) {
|
|
619
|
+
// Found the first subsection, set the position before it
|
|
620
|
+
posBeforeFirstSubsection = parentStartPos + pos
|
|
621
|
+
}
|
|
622
|
+
// Stop descending if we've found the position
|
|
623
|
+
return posBeforeFirstSubsection === null
|
|
624
|
+
})
|
|
625
|
+
break // Stop iterating after finding the parent section
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return posBeforeFirstSubsection
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const findPosAfterParentSection = (
|
|
633
|
+
$pos: ManuscriptResolvedPos
|
|
634
|
+
): number | null => {
|
|
635
|
+
for (let d = $pos.depth; d >= 0; d--) {
|
|
636
|
+
const node = $pos.node(d)
|
|
637
|
+
|
|
638
|
+
if (isSectionNodeType(node.type)) {
|
|
639
|
+
return $pos.after(d)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return null
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const findParentSectionStartPosition = (
|
|
647
|
+
$pos: ManuscriptResolvedPos
|
|
648
|
+
): number | null => {
|
|
649
|
+
for (let d = $pos.depth; d >= 0; d--) {
|
|
650
|
+
const node = $pos.node(d)
|
|
651
|
+
|
|
652
|
+
if (isSectionNodeType(node.type)) {
|
|
653
|
+
return $pos.start(d)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return null
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export const insertSectionLabel = (
|
|
661
|
+
state: ManuscriptEditorState,
|
|
662
|
+
dispatch?: Dispatch,
|
|
663
|
+
currentTr?: Transaction
|
|
664
|
+
) => {
|
|
665
|
+
const pos = findParentSectionStartPosition(state.selection.$from)
|
|
666
|
+
if (pos === null) {
|
|
667
|
+
return false
|
|
668
|
+
}
|
|
669
|
+
const node = state.schema.nodes.section_label.create(
|
|
670
|
+
{},
|
|
671
|
+
state.schema.text('Label')
|
|
672
|
+
)
|
|
673
|
+
const tr = (currentTr || state.tr).insert(pos, node)
|
|
674
|
+
if (dispatch) {
|
|
675
|
+
// place cursor inside section title
|
|
676
|
+
// const selection = TextSelection.create(tr.doc, pos + 2)
|
|
677
|
+
dispatch(tr)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return true
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const isLink = (text: string): boolean => /^\s*(https?:\S+)/.test(text)
|
|
684
|
+
|
|
685
|
+
export const insertLink = (
|
|
686
|
+
state: ManuscriptEditorState,
|
|
687
|
+
dispatch?: Dispatch
|
|
688
|
+
) => {
|
|
689
|
+
const tr = state.tr
|
|
690
|
+
const selection = state.selection
|
|
691
|
+
|
|
692
|
+
if (!selection.empty) {
|
|
693
|
+
const text = selectedText()
|
|
694
|
+
const attrs = {
|
|
695
|
+
href: isLink(text) ? text.trim() : '',
|
|
696
|
+
}
|
|
697
|
+
const range = new NodeRange(
|
|
698
|
+
selection.$from,
|
|
699
|
+
selection.$to,
|
|
700
|
+
selection.$from.depth
|
|
701
|
+
)
|
|
702
|
+
const wrapping = findWrapping(range, schema.nodes.link, attrs)
|
|
703
|
+
|
|
704
|
+
if (wrapping) {
|
|
705
|
+
tr.wrap(range, wrapping)
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
tr.insert(state.selection.anchor, schema.nodes.link.create())
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (dispatch) {
|
|
712
|
+
const selection = NodeSelection.create(tr.doc, state.tr.selection.from)
|
|
713
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export const insertInlineCitation = (
|
|
720
|
+
state: ManuscriptEditorState,
|
|
721
|
+
dispatch?: Dispatch
|
|
722
|
+
) => {
|
|
723
|
+
const node = schema.nodes.citation.create({
|
|
724
|
+
id: generateNodeID(schema.nodes.citation),
|
|
725
|
+
rids: [],
|
|
726
|
+
selectedText: selectedText(),
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
const pos = state.selection.to
|
|
730
|
+
|
|
731
|
+
const { tr } = state
|
|
732
|
+
|
|
733
|
+
tr.insert(pos, node)
|
|
734
|
+
|
|
735
|
+
if (dispatch) {
|
|
736
|
+
const selection = NodeSelection.create(tr.doc, pos)
|
|
737
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return true
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export const insertCrossReference = (
|
|
744
|
+
state: ManuscriptEditorState,
|
|
745
|
+
dispatch?: Dispatch
|
|
746
|
+
) => {
|
|
747
|
+
const node = state.schema.nodes.cross_reference.create({
|
|
748
|
+
rids: [],
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
const pos = state.selection.to
|
|
752
|
+
|
|
753
|
+
const tr = state.tr.insert(pos, node)
|
|
754
|
+
|
|
755
|
+
if (dispatch) {
|
|
756
|
+
const selection = NodeSelection.create(tr.doc, pos)
|
|
757
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return true
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export const insertInlineEquation = (
|
|
764
|
+
state: ManuscriptEditorState,
|
|
765
|
+
dispatch?: Dispatch
|
|
766
|
+
) => {
|
|
767
|
+
const sourcePos = state.selection.from - 1
|
|
768
|
+
|
|
769
|
+
const tr = state.tr.replaceSelectionWith(
|
|
770
|
+
state.schema.nodes.inline_equation.create({
|
|
771
|
+
format: 'tex',
|
|
772
|
+
contents: selectedText().replace(/^\$/, '').replace(/\$$/, ''),
|
|
773
|
+
})
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if (dispatch) {
|
|
777
|
+
const selection = NodeSelection.create(
|
|
778
|
+
tr.doc,
|
|
779
|
+
tr.mapping.map(sourcePos) + 1
|
|
780
|
+
)
|
|
781
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return true
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export const insertTableElementFooter = (
|
|
788
|
+
tr: Transaction,
|
|
789
|
+
table: [ManuscriptNode, number]
|
|
790
|
+
) => {
|
|
791
|
+
const footer = findChildrenByType(
|
|
792
|
+
table[0],
|
|
793
|
+
schema.nodes.table_element_footer
|
|
794
|
+
)[0]
|
|
795
|
+
if (footer) {
|
|
796
|
+
const pos = tr.mapping.map(table[1] + footer.pos)
|
|
797
|
+
if (isDeleted(footer.node)) {
|
|
798
|
+
reinstateNode(tr, footer.node, pos)
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
node: footer.node,
|
|
802
|
+
pos,
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const pos = tr.mapping.map(
|
|
806
|
+
getInsertPos(schema.nodes.table_element_footer, table[0], table[1])
|
|
807
|
+
)
|
|
808
|
+
const node = schema.nodes.table_element_footer.create()
|
|
809
|
+
tr.insert(pos, node)
|
|
810
|
+
return {
|
|
811
|
+
node,
|
|
812
|
+
pos,
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export const insertFootnotesElement = (
|
|
817
|
+
tr: Transaction,
|
|
818
|
+
container: [ManuscriptNode, number]
|
|
819
|
+
) => {
|
|
820
|
+
let pos
|
|
821
|
+
const node = schema.nodes.footnotes_element.create()
|
|
822
|
+
if (isTableElementNode(container[0])) {
|
|
823
|
+
//table footnote
|
|
824
|
+
const footer = insertTableElementFooter(tr, container)
|
|
825
|
+
pos = footer.pos + footer.node.nodeSize - 1
|
|
826
|
+
} else {
|
|
827
|
+
//regular footnote
|
|
828
|
+
const section = insertFootnotesSection(tr)
|
|
829
|
+
pos = section.pos + section.node.nodeSize - 1
|
|
830
|
+
tr.insert(pos, node)
|
|
831
|
+
}
|
|
832
|
+
return [node, pos] as [FootnotesElementNode, number]
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export const insertInlineFootnote = (
|
|
836
|
+
state: ManuscriptEditorState,
|
|
837
|
+
dispatch?: Dispatch
|
|
838
|
+
) => {
|
|
839
|
+
const pos = state.selection.to
|
|
840
|
+
const tr = state.tr
|
|
841
|
+
const container = findFootnotesContainerNode(state.doc, pos)
|
|
842
|
+
const fn = getFootnotesElementState(state, container.node.attrs.id)
|
|
843
|
+
const hasFootnotes =
|
|
844
|
+
fn && fn.footnotes.filter((fn) => !isDeleted(fn[0])).length > 0
|
|
845
|
+
|
|
846
|
+
const footnote = !hasFootnotes && createFootnote()
|
|
847
|
+
const node = schema.nodes.inline_footnote.create({
|
|
848
|
+
rids: footnote ? [footnote.attrs.id] : [],
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
tr.insert(pos, node)
|
|
852
|
+
|
|
853
|
+
if (footnote) {
|
|
854
|
+
let element: [FootnotesElementNode, number]
|
|
855
|
+
if (fn) {
|
|
856
|
+
element = [fn.element[0], tr.mapping.map(fn.element[1])]
|
|
857
|
+
} else {
|
|
858
|
+
element = insertFootnotesElement(tr, [container.node, container.pos])
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (isDeleted(element[0])) {
|
|
862
|
+
reinstateNode(tr, element[0], element[1])
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const fnPos = element[1] + element[0].nodeSize - 1
|
|
866
|
+
tr.insert(fnPos, footnote)
|
|
867
|
+
const selection = TextSelection.create(tr.doc, fnPos + 2)
|
|
868
|
+
tr.setSelection(selection).scrollIntoView()
|
|
869
|
+
} else {
|
|
870
|
+
const selection = NodeSelection.create(tr.doc, pos)
|
|
871
|
+
tr.setSelection(selection).scrollIntoView()
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (dispatch) {
|
|
875
|
+
dispatch(tr)
|
|
876
|
+
}
|
|
877
|
+
return true
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const reinstateNode = (tr: Transaction, node: ManuscriptNode, pos: number) => {
|
|
881
|
+
const attrs = {
|
|
882
|
+
...node.attrs,
|
|
883
|
+
dataTracked: null,
|
|
884
|
+
}
|
|
885
|
+
tr.setNodeMarkup(pos, null, attrs)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export const insertBoxElement = (
|
|
889
|
+
state: ManuscriptEditorState,
|
|
890
|
+
dispatch?: Dispatch
|
|
891
|
+
) => {
|
|
892
|
+
const selection = state.selection
|
|
893
|
+
const { nodes } = schema
|
|
894
|
+
|
|
895
|
+
// Check if the selection is inside the body
|
|
896
|
+
const isBody = hasParentNodeOfType(nodes.body)(selection)
|
|
897
|
+
const isBoxText = hasParentNodeOfType(nodes.box_element)(selection)
|
|
898
|
+
// If selection is not in the body, disable the option
|
|
899
|
+
if (!isBody || isBoxText) {
|
|
900
|
+
return false
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const position = findBlockInsertPosition(state)
|
|
904
|
+
const paragraph = nodes.paragraph.create({})
|
|
905
|
+
|
|
906
|
+
// Create a section node with a section title and a paragraph
|
|
907
|
+
const section = nodes.section.createAndFill({}, [
|
|
908
|
+
nodes.section_title.create(),
|
|
909
|
+
paragraph,
|
|
910
|
+
]) as ManuscriptNode
|
|
911
|
+
|
|
912
|
+
// Create the BoxElement node with a caption_title and the section
|
|
913
|
+
const node = nodes.box_element.createAndFill({}, [
|
|
914
|
+
nodes.caption_title.create(),
|
|
915
|
+
section,
|
|
916
|
+
]) as BoxElementNode
|
|
917
|
+
|
|
918
|
+
if (position && dispatch) {
|
|
919
|
+
const tr = state.tr.insert(position, node)
|
|
920
|
+
const sectionTitlePosition = position + 4
|
|
921
|
+
tr.setSelection(TextSelection.create(tr.doc, sectionTitlePosition))
|
|
922
|
+
dispatch(tr.scrollIntoView())
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return true
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export const insertSection =
|
|
929
|
+
(subsection = false) =>
|
|
930
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
931
|
+
const { nodes } = schema
|
|
932
|
+
const $pos = state.selection.$from
|
|
933
|
+
|
|
934
|
+
const isInBibliography = findParentNodeOfTypeClosestToPos(
|
|
935
|
+
$pos,
|
|
936
|
+
nodes.bibliography_section
|
|
937
|
+
)
|
|
938
|
+
const isInBox = findParentNodeOfTypeClosestToPos($pos, nodes.box_element)
|
|
939
|
+
const isInBody = findParentNodeOfTypeClosestToPos($pos, nodes.body)
|
|
940
|
+
const isInMainTitle = findParentNodeOfTypeClosestToPos($pos, nodes.title)
|
|
941
|
+
|
|
942
|
+
if (isInMainTitle && isBodyLocked(state)) {
|
|
943
|
+
return false
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Disallow inserting (sub)sections in bibliography
|
|
947
|
+
if (isInBibliography) {
|
|
948
|
+
return false
|
|
949
|
+
}
|
|
950
|
+
// Only subsections are allowed in box_element
|
|
951
|
+
if (isInBox && !subsection) {
|
|
952
|
+
return false
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
let insertPos: number | null = null
|
|
956
|
+
|
|
957
|
+
if (subsection) {
|
|
958
|
+
insertPos =
|
|
959
|
+
findPosBeforeFirstSubsection($pos) || findPosAfterParentSection($pos)
|
|
960
|
+
if (!insertPos) {
|
|
961
|
+
return false
|
|
962
|
+
}
|
|
963
|
+
// Move position inside the parent section for the subsection
|
|
964
|
+
insertPos -= 1
|
|
965
|
+
} else {
|
|
966
|
+
if (isInBody) {
|
|
967
|
+
insertPos = findPosAfterParentSection($pos)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (insertPos == null) {
|
|
972
|
+
// Insert at the end of the body if:
|
|
973
|
+
// - selection is outside the body
|
|
974
|
+
// - selection is inside the body but not within a section
|
|
975
|
+
const body = findBody(state.doc)
|
|
976
|
+
insertPos = body.pos + body.node.content.size + 1
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const section = nodes.section.createAndFill() as SectionNode
|
|
980
|
+
const tr = state.tr.insert(insertPos, section)
|
|
981
|
+
|
|
982
|
+
if (dispatch) {
|
|
983
|
+
const selection = TextSelection.create(tr.doc, insertPos + 2) // Place cursor inside section title
|
|
984
|
+
view?.focus()
|
|
985
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return true
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export const insertAbstractSection =
|
|
992
|
+
(category?: SectionCategory) =>
|
|
993
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
994
|
+
if (!category) {
|
|
995
|
+
return false
|
|
996
|
+
}
|
|
997
|
+
const abstracts = findAbstractsNode(state.doc)
|
|
998
|
+
const sections = findChildrenByType(abstracts.node, schema.nodes.section)
|
|
999
|
+
// Check if the section already exists
|
|
1000
|
+
if (sections.some((s) => s.node.attrs.category === category.id)) {
|
|
1001
|
+
return false
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// check if graphical abstract node exist to insert before it.
|
|
1005
|
+
const ga = findChildrenByType(
|
|
1006
|
+
state.doc,
|
|
1007
|
+
schema.nodes.graphical_abstract_section
|
|
1008
|
+
)[0]
|
|
1009
|
+
|
|
1010
|
+
let pos = ga ? ga.pos : abstracts.pos + abstracts.node.content.size + 1
|
|
1011
|
+
if (category.id === 'abstract') {
|
|
1012
|
+
pos = abstracts.pos + 1
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const node = schema.nodes.section.create({ category: category.id }, [
|
|
1016
|
+
schema.nodes.section_title.create({}, schema.text(category.titles[0])),
|
|
1017
|
+
schema.nodes.paragraph.create({ placeholder: 'Type abstract here...' }),
|
|
1018
|
+
])
|
|
1019
|
+
|
|
1020
|
+
const tr = state.tr.insert(pos, node)
|
|
1021
|
+
if (dispatch) {
|
|
1022
|
+
// place cursor inside section title
|
|
1023
|
+
const selection = TextSelection.create(tr.doc, pos)
|
|
1024
|
+
view?.focus()
|
|
1025
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return true
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export const insertBackmatterSection =
|
|
1032
|
+
(category: SectionCategory) =>
|
|
1033
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
1034
|
+
const backmatter = findBackmatter(state.doc)
|
|
1035
|
+
const sections = findChildrenByType(backmatter.node, schema.nodes.section)
|
|
1036
|
+
// Check if the section already exists
|
|
1037
|
+
if (sections.some((s) => s.node.attrs.category === category.id)) {
|
|
1038
|
+
return false
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// check if reference node exist to insert before it.
|
|
1042
|
+
const bibliography = findBibliographySection(state.doc)
|
|
1043
|
+
|
|
1044
|
+
// check if footnotes node exist to insert before it.
|
|
1045
|
+
const footnotesSection = findFootnotesSection(state.doc)
|
|
1046
|
+
|
|
1047
|
+
const pos =
|
|
1048
|
+
footnotesSection?.pos ??
|
|
1049
|
+
bibliography?.pos ??
|
|
1050
|
+
backmatter.pos + backmatter.node.content.size + 1
|
|
1051
|
+
|
|
1052
|
+
const attrs = { category: category.id }
|
|
1053
|
+
const node = schema.nodes.section.create(attrs, [
|
|
1054
|
+
schema.nodes.section_title.create({}, schema.text(category.titles[0])),
|
|
1055
|
+
])
|
|
1056
|
+
|
|
1057
|
+
const tr = state.tr.insert(pos, node)
|
|
1058
|
+
if (dispatch) {
|
|
1059
|
+
// place cursor inside section title
|
|
1060
|
+
const selection = TextSelection.create(tr.doc, pos)
|
|
1061
|
+
view?.focus()
|
|
1062
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return true
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const findSelectedList = (selection: Selection) =>
|
|
1069
|
+
(selection instanceof NodeSelection &&
|
|
1070
|
+
selection.node.type === schema.nodes.list && {
|
|
1071
|
+
pos: selection.from,
|
|
1072
|
+
node: selection.node,
|
|
1073
|
+
}) ||
|
|
1074
|
+
findParentNodeOfType([schema.nodes.list])(selection)
|
|
1075
|
+
|
|
1076
|
+
export const insertGraphicalAbstract =
|
|
1077
|
+
(category?: SectionCategory) =>
|
|
1078
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
1079
|
+
if (!category) {
|
|
1080
|
+
return false
|
|
1081
|
+
}
|
|
1082
|
+
const abstracts = findAbstractsNode(state.doc)
|
|
1083
|
+
const sections = findChildrenByType(
|
|
1084
|
+
abstracts.node,
|
|
1085
|
+
schema.nodes.graphical_abstract_section
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
// Check if the section already exists
|
|
1089
|
+
if (sections.some((s) => s.node.attrs.category === category.id)) {
|
|
1090
|
+
return false
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const ga = findChildrenByType(
|
|
1094
|
+
state.doc,
|
|
1095
|
+
schema.nodes.graphical_abstract_section
|
|
1096
|
+
)[0]
|
|
1097
|
+
|
|
1098
|
+
// insert at the end of abstracts section
|
|
1099
|
+
let pos = abstracts.pos + abstracts.node.content.size + 1
|
|
1100
|
+
// abstract-key-image insert before abstract-graphical
|
|
1101
|
+
pos = ga && category.id === 'abstract-key-image' ? ga.pos : pos
|
|
1102
|
+
|
|
1103
|
+
const node = schema.nodes.graphical_abstract_section.createAndFill(
|
|
1104
|
+
{ category: category.id },
|
|
1105
|
+
[
|
|
1106
|
+
schema.nodes.section_title.create({}, schema.text(category.titles[0])),
|
|
1107
|
+
createAndFillFigureElement(state),
|
|
1108
|
+
]
|
|
1109
|
+
) as GraphicalAbstractSectionNode
|
|
1110
|
+
|
|
1111
|
+
const tr = state.tr.insert(pos, node)
|
|
1112
|
+
node.lastChild && expandAccessibilitySection(tr, node.lastChild)
|
|
1113
|
+
if (dispatch) {
|
|
1114
|
+
// place cursor inside section title
|
|
1115
|
+
const selection = TextSelection.create(tr.doc, pos + 1)
|
|
1116
|
+
if (view) {
|
|
1117
|
+
view.focus()
|
|
1118
|
+
}
|
|
1119
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1120
|
+
}
|
|
1121
|
+
return true
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export const insertContributors = (
|
|
1125
|
+
state: ManuscriptEditorState,
|
|
1126
|
+
dispatch?: Dispatch,
|
|
1127
|
+
view?: EditorView
|
|
1128
|
+
) => {
|
|
1129
|
+
const tr = state.tr
|
|
1130
|
+
|
|
1131
|
+
let contributors = findChildrenByType(state.doc, schema.nodes.contributors)[0]
|
|
1132
|
+
|
|
1133
|
+
if (contributors?.node.childCount) {
|
|
1134
|
+
return false
|
|
1135
|
+
}
|
|
1136
|
+
if (!contributors) {
|
|
1137
|
+
const pos = findInsertionPosition(schema.nodes.contributors, state.doc)
|
|
1138
|
+
const contributorsNode = state.schema.nodes.contributors.create({
|
|
1139
|
+
id: generateNodeID(schema.nodes.contributors),
|
|
1140
|
+
})
|
|
1141
|
+
tr.insert(pos, contributorsNode)
|
|
1142
|
+
contributors = { node: contributorsNode, pos }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (dispatch) {
|
|
1146
|
+
const selection = NodeSelection.create(tr.doc, contributors.pos)
|
|
1147
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1148
|
+
openAuthorsAndAffiliationsModals(contributors.pos, view, 'authors')
|
|
1149
|
+
}
|
|
1150
|
+
return true
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
export const insertAffiliation = (
|
|
1154
|
+
state: ManuscriptEditorState,
|
|
1155
|
+
dispatch?: Dispatch,
|
|
1156
|
+
view?: EditorView
|
|
1157
|
+
) => {
|
|
1158
|
+
const tr = state.tr
|
|
1159
|
+
|
|
1160
|
+
let affiliations = findChildrenByType(state.doc, schema.nodes.affiliations)[0]
|
|
1161
|
+
|
|
1162
|
+
if (affiliations?.node.childCount) {
|
|
1163
|
+
return false
|
|
1164
|
+
}
|
|
1165
|
+
if (!affiliations) {
|
|
1166
|
+
const pos = findInsertionPosition(schema.nodes.affiliations, state.doc)
|
|
1167
|
+
const affiliationsNode = state.schema.nodes.affiliations.create({
|
|
1168
|
+
id: generateNodeID(schema.nodes.affiliations),
|
|
1169
|
+
})
|
|
1170
|
+
tr.insert(pos, affiliationsNode)
|
|
1171
|
+
affiliations = { node: affiliationsNode, pos }
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (dispatch) {
|
|
1175
|
+
const selection = NodeSelection.create(tr.doc, affiliations.pos)
|
|
1176
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1177
|
+
openAuthorsAndAffiliationsModals(affiliations.pos, view, 'affiliations')
|
|
1178
|
+
}
|
|
1179
|
+
return true
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
export const insertAward =
|
|
1183
|
+
(attrs?: AwardAttrs) =>
|
|
1184
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
1185
|
+
const award = schema.nodes.award.create(attrs) as AwardNode
|
|
1186
|
+
const tr = state.tr
|
|
1187
|
+
const awards = insertAwardsNode(tr)
|
|
1188
|
+
const pos = awards.pos + awards.node.nodeSize - 1
|
|
1189
|
+
tr.insert(pos, award)
|
|
1190
|
+
const selection = NodeSelection.create(tr.doc, pos)
|
|
1191
|
+
if (dispatch && view) {
|
|
1192
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1193
|
+
view.focus()
|
|
1194
|
+
}
|
|
1195
|
+
return true
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export const insertKeywords = (
|
|
1199
|
+
state: ManuscriptEditorState,
|
|
1200
|
+
dispatch?: Dispatch,
|
|
1201
|
+
view?: EditorView
|
|
1202
|
+
) => {
|
|
1203
|
+
// Check if another keywords node already exists
|
|
1204
|
+
if (getChildOfType(state.doc, schema.nodes.keywords, true)) {
|
|
1205
|
+
return false
|
|
1206
|
+
}
|
|
1207
|
+
const pos = findInsertionPosition(schema.nodes.keywords, state.doc)
|
|
1208
|
+
const keywords = schema.nodes.keywords.createAndFill({}, [
|
|
1209
|
+
schema.nodes.section_title.create({}, schema.text('Keywords')),
|
|
1210
|
+
schema.nodes.keywords_element.create({}, [
|
|
1211
|
+
schema.nodes.keyword_group.create({}, []),
|
|
1212
|
+
]),
|
|
1213
|
+
]) as KeywordsNode
|
|
1214
|
+
|
|
1215
|
+
const tr = state.tr.insert(pos, keywords)
|
|
1216
|
+
|
|
1217
|
+
if (dispatch) {
|
|
1218
|
+
const selection = NodeSelection.create(tr.doc, pos)
|
|
1219
|
+
if (view) {
|
|
1220
|
+
view.focus()
|
|
1221
|
+
}
|
|
1222
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return true
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const findRootList = ($pos: ResolvedPos) => {
|
|
1229
|
+
for (let i = 0; i < $pos.depth; i++) {
|
|
1230
|
+
const node = $pos.node(i)
|
|
1231
|
+
if (isListNode(node)) {
|
|
1232
|
+
const pos = $pos.start(i)
|
|
1233
|
+
return {
|
|
1234
|
+
node,
|
|
1235
|
+
pos,
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
//Somewhat expensive logic, should this be in a plugin?
|
|
1242
|
+
const findListsAtSameLevel = (doc: ManuscriptNode, list: NodeWithPos) => {
|
|
1243
|
+
const $pos = doc.resolve(list.pos + 1)
|
|
1244
|
+
// find the top-level list. This is an optimization to
|
|
1245
|
+
// avoid traversing the entire document looking for lists
|
|
1246
|
+
const root = findRootList($pos)
|
|
1247
|
+
if (!root) {
|
|
1248
|
+
return [list]
|
|
1249
|
+
}
|
|
1250
|
+
const target = $pos.depth
|
|
1251
|
+
const lists: NodeWithPos[] = []
|
|
1252
|
+
root.node.descendants((node, pos) => {
|
|
1253
|
+
const $pos = doc.resolve(root.pos + pos + 1)
|
|
1254
|
+
if ($pos.depth === target && isListNode(node)) {
|
|
1255
|
+
lists.push({ node, pos: $pos.before(target) })
|
|
1256
|
+
}
|
|
1257
|
+
return $pos.depth <= target
|
|
1258
|
+
})
|
|
1259
|
+
return lists
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function toggleOffList(
|
|
1263
|
+
state: EditorState,
|
|
1264
|
+
dispatch: (tr: ManuscriptTransaction) => void
|
|
1265
|
+
) {
|
|
1266
|
+
const {
|
|
1267
|
+
selection: { $from },
|
|
1268
|
+
tr,
|
|
1269
|
+
} = state
|
|
1270
|
+
|
|
1271
|
+
let rootList = findRootList($from)
|
|
1272
|
+
rootList && rootList.pos--
|
|
1273
|
+
|
|
1274
|
+
if (
|
|
1275
|
+
state.selection instanceof NodeSelection &&
|
|
1276
|
+
state.selection.node.type === schema.nodes.list
|
|
1277
|
+
) {
|
|
1278
|
+
rootList = {
|
|
1279
|
+
pos: state.selection.from,
|
|
1280
|
+
node: state.selection.node as ListNode,
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (rootList) {
|
|
1285
|
+
state.doc.nodesBetween(
|
|
1286
|
+
rootList.pos,
|
|
1287
|
+
rootList.pos + rootList.node.nodeSize,
|
|
1288
|
+
(node, pos) => {
|
|
1289
|
+
// remove all the nodes that are not fully in the range
|
|
1290
|
+
if (
|
|
1291
|
+
rootList &&
|
|
1292
|
+
(pos < rootList.pos ||
|
|
1293
|
+
node.nodeSize > rootList.pos + rootList.node.nodeSize)
|
|
1294
|
+
) {
|
|
1295
|
+
return true
|
|
1296
|
+
}
|
|
1297
|
+
if (node.type === schema.nodes.paragraph) {
|
|
1298
|
+
tr.insert(tr.mapping.map(rootList.pos), node)
|
|
1299
|
+
return false
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
)
|
|
1303
|
+
tr.delete(
|
|
1304
|
+
tr.mapping.map(rootList.pos),
|
|
1305
|
+
tr.mapping.map(rootList.pos + rootList.node.nodeSize)
|
|
1306
|
+
)
|
|
1307
|
+
dispatch(tr)
|
|
1308
|
+
return true
|
|
1309
|
+
} else {
|
|
1310
|
+
return false
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
export const insertList =
|
|
1315
|
+
(type: ManuscriptNodeType, style?: string) =>
|
|
1316
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
1317
|
+
const list = findSelectedList(state.selection)
|
|
1318
|
+
|
|
1319
|
+
if (list) {
|
|
1320
|
+
if (!dispatch) {
|
|
1321
|
+
return true
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (list.node.attrs.listStyleType === style) {
|
|
1325
|
+
return toggleOffList(state, dispatch)
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// list was found: update the type and style
|
|
1329
|
+
// of every list at the same level
|
|
1330
|
+
const nodes = findListsAtSameLevel(state.doc, list)
|
|
1331
|
+
const tr = state.tr
|
|
1332
|
+
for (const { node, pos } of nodes) {
|
|
1333
|
+
tr.setNodeMarkup(
|
|
1334
|
+
pos,
|
|
1335
|
+
type,
|
|
1336
|
+
{
|
|
1337
|
+
...node.attrs,
|
|
1338
|
+
listStyleType: style,
|
|
1339
|
+
},
|
|
1340
|
+
node.marks
|
|
1341
|
+
)
|
|
1342
|
+
}
|
|
1343
|
+
dispatch(tr)
|
|
1344
|
+
return true
|
|
1345
|
+
} else {
|
|
1346
|
+
// no list found, create new list
|
|
1347
|
+
const { selection } = state
|
|
1348
|
+
let tr = state.tr
|
|
1349
|
+
const startPosition = selection.$from.pos + 1
|
|
1350
|
+
|
|
1351
|
+
return wrapInList(type, { listStyleType: style })(state, (tempTr) => {
|
|
1352
|
+
// if we dispatch all steps in this transaction track-changes-plugin will not be able to revert ReplaceAroundStep
|
|
1353
|
+
// as we have another ReplaceStep that will make transaction more complicated, so to make it easy to tracker we dispatch first ReplaceAroundStep
|
|
1354
|
+
// then will dispatch reminder steps in one transaction
|
|
1355
|
+
const range = selection.$from.blockRange(selection.$to)
|
|
1356
|
+
if (range && dispatch) {
|
|
1357
|
+
tempTr.steps.map((step) => {
|
|
1358
|
+
if (step instanceof ReplaceAroundStep) {
|
|
1359
|
+
dispatch(tr.step(step))
|
|
1360
|
+
tr = view?.state.tr || tr
|
|
1361
|
+
} else {
|
|
1362
|
+
tr.step(step)
|
|
1363
|
+
}
|
|
1364
|
+
})
|
|
1365
|
+
if (startPosition) {
|
|
1366
|
+
const selection = createSelection(
|
|
1367
|
+
state.schema.nodes.paragraph,
|
|
1368
|
+
startPosition,
|
|
1369
|
+
tr.doc
|
|
1370
|
+
)
|
|
1371
|
+
view?.focus()
|
|
1372
|
+
tr.setSelection(selection)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
dispatch(tr)
|
|
1376
|
+
}
|
|
1377
|
+
})
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
export const insertBibliographySection = () => {
|
|
1382
|
+
return false
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
export const insertTOCSection = () => {
|
|
1386
|
+
return false
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
export const insertTransAbstract = (
|
|
1390
|
+
state: ManuscriptEditorState,
|
|
1391
|
+
dispatch?: Dispatch,
|
|
1392
|
+
category?: string,
|
|
1393
|
+
insertAfterPos?: number
|
|
1394
|
+
) => {
|
|
1395
|
+
if (!templateAllows(state, schema.nodes.trans_abstract)) {
|
|
1396
|
+
return false
|
|
1397
|
+
}
|
|
1398
|
+
if (!dispatch) {
|
|
1399
|
+
return true
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Get document's primary language or default to English
|
|
1403
|
+
const lang = state.doc.attrs.primaryLanguageCode || 'en'
|
|
1404
|
+
|
|
1405
|
+
// Create empty section title
|
|
1406
|
+
const title = schema.nodes.section_title.create()
|
|
1407
|
+
// Create empty paragraph
|
|
1408
|
+
const paragraph = schema.nodes.paragraph.create()
|
|
1409
|
+
|
|
1410
|
+
// Create trans_abstract node with section title and paragraph
|
|
1411
|
+
// Pass the current section's category to the trans_abstract
|
|
1412
|
+
const node = schema.nodes.trans_abstract.create(
|
|
1413
|
+
{
|
|
1414
|
+
lang,
|
|
1415
|
+
category,
|
|
1416
|
+
},
|
|
1417
|
+
[title, paragraph]
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
const abstracts = findAbstractsNode(state.doc)
|
|
1421
|
+
|
|
1422
|
+
const pos =
|
|
1423
|
+
insertAfterPos != null
|
|
1424
|
+
? insertAfterPos
|
|
1425
|
+
: abstracts.pos + abstracts.node.nodeSize - 1
|
|
1426
|
+
const tr = state.tr.insert(pos, node)
|
|
1427
|
+
|
|
1428
|
+
const selection = TextSelection.create(tr.doc, pos + 1)
|
|
1429
|
+
tr.setSelection(selection).scrollIntoView()
|
|
1430
|
+
|
|
1431
|
+
dispatch(tr)
|
|
1432
|
+
return true
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
export const insertTransGraphicalAbstract =
|
|
1436
|
+
(category: SectionCategory, insertAfterPos?: number) =>
|
|
1437
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
1438
|
+
if (!templateAllows(state, schema.nodes.trans_graphical_abstract)) {
|
|
1439
|
+
return false
|
|
1440
|
+
}
|
|
1441
|
+
if (!dispatch) {
|
|
1442
|
+
return true
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const lang = state.doc.attrs.primaryLanguageCode || 'en'
|
|
1446
|
+
|
|
1447
|
+
const abstracts = findAbstractsNode(state.doc)
|
|
1448
|
+
const pos =
|
|
1449
|
+
insertAfterPos != null
|
|
1450
|
+
? insertAfterPos
|
|
1451
|
+
: abstracts.pos + abstracts.node.content.size + 1
|
|
1452
|
+
|
|
1453
|
+
const node = schema.nodes.trans_graphical_abstract.createAndFill(
|
|
1454
|
+
{
|
|
1455
|
+
lang,
|
|
1456
|
+
category: category.id,
|
|
1457
|
+
},
|
|
1458
|
+
[
|
|
1459
|
+
schema.nodes.section_title.create({}, schema.text(category.titles[0])),
|
|
1460
|
+
createAndFillFigureElement(state),
|
|
1461
|
+
]
|
|
1462
|
+
) as TransGraphicalAbstractNode
|
|
1463
|
+
|
|
1464
|
+
const tr = state.tr.insert(pos, node)
|
|
1465
|
+
if (node.lastChild) {
|
|
1466
|
+
expandAccessibilitySection(tr, node.lastChild)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const selection = TextSelection.create(tr.doc, pos + 1)
|
|
1470
|
+
if (view) {
|
|
1471
|
+
view.focus()
|
|
1472
|
+
}
|
|
1473
|
+
dispatch(tr.setSelection(selection).scrollIntoView())
|
|
1474
|
+
return true
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Copied from prosemirror-commands
|
|
1478
|
+
const findCutBefore = ($pos: ResolvedPos) => {
|
|
1479
|
+
if (!$pos.parent.type.spec.isolating) {
|
|
1480
|
+
for (let i = $pos.depth - 1; i >= 0; i--) {
|
|
1481
|
+
if ($pos.index(i) > 0) {
|
|
1482
|
+
return $pos.doc.resolve($pos.before(i + 1))
|
|
1483
|
+
}
|
|
1484
|
+
if ($pos.node(i).type.spec.isolating) {
|
|
1485
|
+
break
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return null
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
export const isAtStartOfTextBlock = (
|
|
1493
|
+
state: ManuscriptEditorState,
|
|
1494
|
+
$cursor: ResolvedPos,
|
|
1495
|
+
view?: ManuscriptEditorView
|
|
1496
|
+
) => (view ? view.endOfTextblock('backward', state) : $cursor.parentOffset <= 0)
|
|
1497
|
+
|
|
1498
|
+
export const isTextSelection = (
|
|
1499
|
+
selection: Selection
|
|
1500
|
+
): selection is ManuscriptTextSelection => selection instanceof TextSelection
|
|
1501
|
+
|
|
1502
|
+
// Ignore atom blocks (as backspace handler), instead of deleting them.
|
|
1503
|
+
// Adapted from selectNodeBackward in prosemirror-commands
|
|
1504
|
+
export const ignoreAtomBlockNodeBackward = (
|
|
1505
|
+
state: ManuscriptEditorState,
|
|
1506
|
+
dispatch?: Dispatch,
|
|
1507
|
+
view?: ManuscriptEditorView
|
|
1508
|
+
): boolean => {
|
|
1509
|
+
const { selection } = state
|
|
1510
|
+
|
|
1511
|
+
if (!isTextSelection(selection)) {
|
|
1512
|
+
return false
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const { $cursor } = selection
|
|
1516
|
+
|
|
1517
|
+
if (!$cursor) {
|
|
1518
|
+
return false
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// ignore empty blocks
|
|
1522
|
+
if ($cursor.parent.content.size === 0) {
|
|
1523
|
+
return false
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// handle cursor at start of textblock
|
|
1527
|
+
if (!isAtStartOfTextBlock(state, $cursor, view)) {
|
|
1528
|
+
return false
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const $cut = findCutBefore($cursor)
|
|
1532
|
+
|
|
1533
|
+
if (!$cut) {
|
|
1534
|
+
return false
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const node = $cut.nodeBefore
|
|
1538
|
+
|
|
1539
|
+
if (!node) {
|
|
1540
|
+
return false
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
return node.isBlock && node.isAtom
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
export const ignoreMetaNodeBackspaceCommand = (
|
|
1547
|
+
state: ManuscriptEditorState
|
|
1548
|
+
) => {
|
|
1549
|
+
const { selection } = state
|
|
1550
|
+
|
|
1551
|
+
if (!isNodeSelection(selection)) {
|
|
1552
|
+
return false
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return (
|
|
1556
|
+
selection.node.type === schema.nodes.keyword_group ||
|
|
1557
|
+
selection.node.type === schema.nodes.keyword ||
|
|
1558
|
+
selection.node.type === schema.nodes.affiliations ||
|
|
1559
|
+
selection.node.type === schema.nodes.affiliation ||
|
|
1560
|
+
selection.node.type === schema.nodes.contributors ||
|
|
1561
|
+
selection.node.type === schema.nodes.contributor ||
|
|
1562
|
+
selection.node.type === schema.nodes.awards ||
|
|
1563
|
+
selection.node.type === schema.nodes.award
|
|
1564
|
+
)
|
|
1565
|
+
}
|
|
1566
|
+
// Copied from prosemirror-commands
|
|
1567
|
+
const findCutAfter = ($pos: ResolvedPos) => {
|
|
1568
|
+
if (!$pos.parent.type.spec.isolating) {
|
|
1569
|
+
for (let i = $pos.depth - 1; i >= 0; i--) {
|
|
1570
|
+
const parent = $pos.node(i)
|
|
1571
|
+
if ($pos.index(i) + 1 < parent.childCount) {
|
|
1572
|
+
return $pos.doc.resolve($pos.after(i + 1))
|
|
1573
|
+
}
|
|
1574
|
+
if (parent.type.spec.isolating) {
|
|
1575
|
+
break
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return null
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
export const isAtEndOfTextBlock = (
|
|
1583
|
+
state: ManuscriptEditorState,
|
|
1584
|
+
$cursor: ResolvedPos,
|
|
1585
|
+
view?: ManuscriptEditorView
|
|
1586
|
+
) =>
|
|
1587
|
+
view
|
|
1588
|
+
? view.endOfTextblock('forward', state)
|
|
1589
|
+
: $cursor.parentOffset >= $cursor.parent.content.size
|
|
1590
|
+
|
|
1591
|
+
// Ignore atom blocks (as delete handler), instead of deleting them.
|
|
1592
|
+
// Adapted from selectNodeForward in prosemirror-commands
|
|
1593
|
+
export const ignoreAtomBlockNodeForward = (
|
|
1594
|
+
state: ManuscriptEditorState,
|
|
1595
|
+
dispatch?: Dispatch,
|
|
1596
|
+
view?: ManuscriptEditorView
|
|
1597
|
+
): boolean => {
|
|
1598
|
+
const { selection } = state
|
|
1599
|
+
|
|
1600
|
+
if (!isTextSelection(selection)) {
|
|
1601
|
+
return false
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const { $cursor } = selection
|
|
1605
|
+
|
|
1606
|
+
if (!$cursor) {
|
|
1607
|
+
return false
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// ignore empty blocks
|
|
1611
|
+
if ($cursor.parent.content.size === 0) {
|
|
1612
|
+
return false
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// handle cursor at start of textblock
|
|
1616
|
+
if (!isAtEndOfTextBlock(state, $cursor, view)) {
|
|
1617
|
+
return false
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const $cut = findCutAfter($cursor)
|
|
1621
|
+
|
|
1622
|
+
if (!$cut) {
|
|
1623
|
+
return false
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const node = $cut.nodeAfter
|
|
1627
|
+
|
|
1628
|
+
if (!node) {
|
|
1629
|
+
return false
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
return node.isBlock && node.isAtom
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const selectIsolatingParent = (
|
|
1636
|
+
state: ManuscriptEditorState
|
|
1637
|
+
): TextSelection | null => {
|
|
1638
|
+
const { $anchor } = state.selection
|
|
1639
|
+
|
|
1640
|
+
for (let d = $anchor.depth; d >= 0; d--) {
|
|
1641
|
+
const node = $anchor.node(d)
|
|
1642
|
+
|
|
1643
|
+
if (node.type.spec.isolating) {
|
|
1644
|
+
return TextSelection.create(
|
|
1645
|
+
state.tr.doc,
|
|
1646
|
+
$anchor.start(d),
|
|
1647
|
+
$anchor.end(d)
|
|
1648
|
+
)
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return null
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* "Select All" the contents of an isolating block instead of the whole document
|
|
1657
|
+
*/
|
|
1658
|
+
export const selectAllIsolating = (
|
|
1659
|
+
state: ManuscriptEditorState,
|
|
1660
|
+
dispatch?: Dispatch
|
|
1661
|
+
): boolean => {
|
|
1662
|
+
const selection = selectIsolatingParent(state)
|
|
1663
|
+
|
|
1664
|
+
if (!selection) {
|
|
1665
|
+
return false
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (dispatch) {
|
|
1669
|
+
dispatch(state.tr.setSelection(selection))
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
return true
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
export type TableConfig = {
|
|
1676
|
+
numberOfColumns: number
|
|
1677
|
+
numberOfRows: number
|
|
1678
|
+
includeHeader: boolean
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const DEFAULT_TABLE_CONFIG: TableConfig = {
|
|
1682
|
+
numberOfColumns: 2,
|
|
1683
|
+
numberOfRows: 2,
|
|
1684
|
+
includeHeader: true,
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Create a table containing a configurable number of rows and columns.
|
|
1689
|
+
* The table can optionally include a header row.
|
|
1690
|
+
*/
|
|
1691
|
+
export const createAndFillTableElement = (
|
|
1692
|
+
attrs?: Attrs,
|
|
1693
|
+
config = DEFAULT_TABLE_CONFIG
|
|
1694
|
+
) => {
|
|
1695
|
+
const { numberOfColumns, numberOfRows, includeHeader } = config
|
|
1696
|
+
const createRow = (cellType: ManuscriptNodeType) => {
|
|
1697
|
+
const cells = Array.from({ length: numberOfColumns }, () =>
|
|
1698
|
+
cellType.create({}, schema.nodes.text_block.create())
|
|
1699
|
+
)
|
|
1700
|
+
return schema.nodes.table_row.create({}, cells)
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
const tableRows = includeHeader ? [createRow(schema.nodes.table_header)] : []
|
|
1704
|
+
|
|
1705
|
+
for (let i = 0; i < numberOfRows; i++) {
|
|
1706
|
+
tableRows.push(createRow(schema.nodes.table_cell))
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
return schema.nodes.table_element.createChecked(
|
|
1710
|
+
{
|
|
1711
|
+
...attrs,
|
|
1712
|
+
id: generateNodeID(schema.nodes.table_element),
|
|
1713
|
+
},
|
|
1714
|
+
[
|
|
1715
|
+
schema.nodes.caption_title.create(),
|
|
1716
|
+
schema.nodes.table.create({}, tableRows),
|
|
1717
|
+
schema.nodes.alt_text.create(),
|
|
1718
|
+
schema.nodes.long_desc.create(),
|
|
1719
|
+
schema.nodes.listing.create(),
|
|
1720
|
+
]
|
|
1721
|
+
)
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const createAndFillFigureElement = (attrs?: Attrs) =>
|
|
1725
|
+
schema.nodes.figure_element.create(
|
|
1726
|
+
{
|
|
1727
|
+
...attrs,
|
|
1728
|
+
id: generateNodeID(schema.nodes.figure_element),
|
|
1729
|
+
},
|
|
1730
|
+
[
|
|
1731
|
+
schema.nodes.figure.create(),
|
|
1732
|
+
schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
|
|
1733
|
+
schema.nodes.alt_text.create(),
|
|
1734
|
+
schema.nodes.long_desc.create(),
|
|
1735
|
+
schema.nodes.listing.create(),
|
|
1736
|
+
]
|
|
1737
|
+
)
|
|
1738
|
+
|
|
1739
|
+
const createAndFillCaption = () => [
|
|
1740
|
+
schema.nodes.caption_title.create(),
|
|
1741
|
+
schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
|
|
1742
|
+
]
|
|
1743
|
+
|
|
1744
|
+
const createImageElement = (attrs?: Attrs) =>
|
|
1745
|
+
schema.nodes.image_element.create(
|
|
1746
|
+
{
|
|
1747
|
+
...attrs,
|
|
1748
|
+
id: generateNodeID(schema.nodes.image_element),
|
|
1749
|
+
},
|
|
1750
|
+
[
|
|
1751
|
+
schema.nodes.figure.create(),
|
|
1752
|
+
schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
|
|
1753
|
+
schema.nodes.alt_text.create(),
|
|
1754
|
+
schema.nodes.long_desc.create(),
|
|
1755
|
+
]
|
|
1756
|
+
)
|
|
1757
|
+
|
|
1758
|
+
const createEmbedElement = (attrs?: Attrs) =>
|
|
1759
|
+
schema.nodes.embed.create(
|
|
1760
|
+
{
|
|
1761
|
+
...attrs,
|
|
1762
|
+
id: generateNodeID(schema.nodes.embed),
|
|
1763
|
+
},
|
|
1764
|
+
[
|
|
1765
|
+
...createAndFillCaption(),
|
|
1766
|
+
schema.nodes.alt_text.create(),
|
|
1767
|
+
schema.nodes.long_desc.create(),
|
|
1768
|
+
]
|
|
1769
|
+
)
|
|
1770
|
+
/**
|
|
1771
|
+
* This to make sure we get block node
|
|
1772
|
+
*/
|
|
1773
|
+
const getParentNode = (selection: Selection) => {
|
|
1774
|
+
const parentNode = findParentNodeWithId(selection)
|
|
1775
|
+
const node = parentNode?.node
|
|
1776
|
+
|
|
1777
|
+
if (node?.type === schema.nodes.table) {
|
|
1778
|
+
return findParentNodeOfType(schema.nodes.table_element)(selection)?.node
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return node
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
export const addNodeComment = (
|
|
1785
|
+
node: ManuscriptNode,
|
|
1786
|
+
state: ManuscriptEditorState,
|
|
1787
|
+
dispatch?: Dispatch
|
|
1788
|
+
) => {
|
|
1789
|
+
const props = getEditorProps(state)
|
|
1790
|
+
const attrs = {
|
|
1791
|
+
id: generateNodeID(schema.nodes.comment),
|
|
1792
|
+
contents: '',
|
|
1793
|
+
target: node.attrs.id,
|
|
1794
|
+
userID: props.userID,
|
|
1795
|
+
timestamp: Date.now(),
|
|
1796
|
+
} as CommentAttrs
|
|
1797
|
+
const comment = schema.nodes.comment.create(attrs)
|
|
1798
|
+
const comments = findChildrenByType(state.doc, schema.nodes.comments)[0]
|
|
1799
|
+
if (comments) {
|
|
1800
|
+
const pos = comments.pos + 1
|
|
1801
|
+
|
|
1802
|
+
const tr = state.tr.insert(pos, comment)
|
|
1803
|
+
const key = getCommentKey(attrs, undefined, node)
|
|
1804
|
+
setCommentSelection(tr, key, attrs.id, true)
|
|
1805
|
+
if (dispatch) {
|
|
1806
|
+
dispatch(tr)
|
|
1807
|
+
}
|
|
1808
|
+
return true
|
|
1809
|
+
}
|
|
1810
|
+
return false
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
export const addInlineComment = (
|
|
1814
|
+
state: ManuscriptEditorState,
|
|
1815
|
+
dispatch?: Dispatch
|
|
1816
|
+
): boolean => {
|
|
1817
|
+
const selection = state.selection
|
|
1818
|
+
const node = getParentNode(selection)
|
|
1819
|
+
if (!node) {
|
|
1820
|
+
return false
|
|
1821
|
+
}
|
|
1822
|
+
let from = selection.from
|
|
1823
|
+
let to = selection.to
|
|
1824
|
+
|
|
1825
|
+
if (from === to) {
|
|
1826
|
+
// Use the current cursor position to determine the boundaries of the intended word
|
|
1827
|
+
const result = findWordBoundaries(state, from)
|
|
1828
|
+
from = result.from
|
|
1829
|
+
to = result.to
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const props = getEditorProps(state)
|
|
1833
|
+
const attrs = {
|
|
1834
|
+
id: generateNodeID(schema.nodes.comment),
|
|
1835
|
+
contents: '',
|
|
1836
|
+
target: node.attrs.id,
|
|
1837
|
+
userID: props.userID,
|
|
1838
|
+
timestamp: Date.now(),
|
|
1839
|
+
originalText: selectedText() || state.doc.textBetween(from, to),
|
|
1840
|
+
selector: {
|
|
1841
|
+
from,
|
|
1842
|
+
to,
|
|
1843
|
+
},
|
|
1844
|
+
} as CommentAttrs
|
|
1845
|
+
const comment = schema.nodes.comment.create(attrs)
|
|
1846
|
+
const comments = findChildrenByType(state.doc, schema.nodes.comments)[0]
|
|
1847
|
+
if (comments) {
|
|
1848
|
+
const pos = comments.pos + 1
|
|
1849
|
+
|
|
1850
|
+
const tr = state.tr.insert(pos, comment)
|
|
1851
|
+
|
|
1852
|
+
const start = schema.nodes.highlight_marker.create({
|
|
1853
|
+
id: comment.attrs.id,
|
|
1854
|
+
tid: node.attrs.id,
|
|
1855
|
+
position: 'start',
|
|
1856
|
+
})
|
|
1857
|
+
const end = schema.nodes.highlight_marker.create({
|
|
1858
|
+
id: comment.attrs.id,
|
|
1859
|
+
tid: node.attrs.id,
|
|
1860
|
+
position: 'end',
|
|
1861
|
+
})
|
|
1862
|
+
tr.insert(from, start).insert(to + 1, end)
|
|
1863
|
+
|
|
1864
|
+
const range = getCommentRange(attrs)
|
|
1865
|
+
const key = getCommentKey(attrs, range, node)
|
|
1866
|
+
setCommentSelection(tr, key, attrs.id, true)
|
|
1867
|
+
if (dispatch) {
|
|
1868
|
+
dispatch(tr)
|
|
1869
|
+
}
|
|
1870
|
+
return true
|
|
1871
|
+
}
|
|
1872
|
+
return false
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
export const addRows =
|
|
1876
|
+
(direction: 'top' | 'bottom') =>
|
|
1877
|
+
(state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
|
|
1878
|
+
if (dispatch) {
|
|
1879
|
+
const { tr } = state
|
|
1880
|
+
let rect = selectedRect(state)
|
|
1881
|
+
const selectedRows = rect.bottom - rect.top
|
|
1882
|
+
for (let i = 0; i < selectedRows; i++) {
|
|
1883
|
+
addRow(tr, rect, rect[direction])
|
|
1884
|
+
// this to make sure next row has tracking attributes, so each step of addRow has a different location
|
|
1885
|
+
rect = {
|
|
1886
|
+
...selectedRect(state.apply(tr)),
|
|
1887
|
+
top: rect.top + 1,
|
|
1888
|
+
bottom: rect.bottom + 1,
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
dispatch(tr)
|
|
1892
|
+
}
|
|
1893
|
+
return true
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
export const addHeaderRow =
|
|
1897
|
+
(direction: 'above' | 'below') =>
|
|
1898
|
+
(state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
|
|
1899
|
+
if (dispatch) {
|
|
1900
|
+
const { tr } = state
|
|
1901
|
+
const rect = selectedRect(state)
|
|
1902
|
+
const addRowStep = addRow(
|
|
1903
|
+
state.tr,
|
|
1904
|
+
rect,
|
|
1905
|
+
rect[direction === 'below' ? 'bottom' : 'top']
|
|
1906
|
+
).steps.pop()
|
|
1907
|
+
if (addRowStep && addRowStep instanceof ReplaceStep) {
|
|
1908
|
+
const { from, to, slice } = addRowStep
|
|
1909
|
+
const cells = flatten(slice.content.firstChild as ManuscriptNode, false)
|
|
1910
|
+
const row = schema.nodes.table_row.create(
|
|
1911
|
+
undefined,
|
|
1912
|
+
cells.map((cell) =>
|
|
1913
|
+
schema.nodes.table_header.create(cell.node.attrs, cell.node.content)
|
|
1914
|
+
)
|
|
1915
|
+
)
|
|
1916
|
+
tr.step(
|
|
1917
|
+
new ReplaceStep(
|
|
1918
|
+
from,
|
|
1919
|
+
to,
|
|
1920
|
+
new Slice(Fragment.from(row), slice.openStart, slice.openEnd)
|
|
1921
|
+
)
|
|
1922
|
+
)
|
|
1923
|
+
dispatch(tr)
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return true
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
export const addColumns =
|
|
1930
|
+
(direction: 'right' | 'left') =>
|
|
1931
|
+
(state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
|
|
1932
|
+
if (dispatch) {
|
|
1933
|
+
const { tr } = state
|
|
1934
|
+
const rect = selectedRect(state.apply(tr))
|
|
1935
|
+
const selectedRows = rect.right - rect.left
|
|
1936
|
+
for (let i = 0; i < selectedRows; i++) {
|
|
1937
|
+
const command = direction === 'right' ? addColumnAfter : addColumnBefore
|
|
1938
|
+
command(state.apply(tr), (t) => t.steps.map((s) => tr.step(s)))
|
|
1939
|
+
}
|
|
1940
|
+
dispatch(tr)
|
|
1941
|
+
}
|
|
1942
|
+
return true
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
export const autoComplete = (
|
|
1946
|
+
state: ManuscriptEditorState,
|
|
1947
|
+
dispatch?: Dispatch
|
|
1948
|
+
) => {
|
|
1949
|
+
const complete = checkForCompletion(state)
|
|
1950
|
+
if (complete && complete.suggestion.length > 0) {
|
|
1951
|
+
const tr = state.tr.insertText(complete.suggestion, state.selection.from)
|
|
1952
|
+
const inserted = complete.title.substring(
|
|
1953
|
+
0,
|
|
1954
|
+
complete.title.length - complete.suggestion.length
|
|
1955
|
+
)
|
|
1956
|
+
if (inserted) {
|
|
1957
|
+
// replacing to provide text case as required
|
|
1958
|
+
tr.replaceWith(
|
|
1959
|
+
state.selection.from - inserted.length,
|
|
1960
|
+
state.selection.from,
|
|
1961
|
+
schema.text(inserted)
|
|
1962
|
+
)
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
dispatch && dispatch(tr)
|
|
1966
|
+
return true
|
|
1967
|
+
}
|
|
1968
|
+
return false
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
export const activateSearch = (
|
|
1972
|
+
state: ManuscriptEditorState,
|
|
1973
|
+
dispatch?: Dispatch
|
|
1974
|
+
) => {
|
|
1975
|
+
const pluginState = searchReplaceKey.getState(state)
|
|
1976
|
+
const tr = state.tr.setMeta(searchReplaceKey, {
|
|
1977
|
+
active: !pluginState?.active,
|
|
1978
|
+
})
|
|
1979
|
+
dispatch && dispatch(tr)
|
|
1980
|
+
return true
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
export const activateSearchReplace = (
|
|
1984
|
+
state: ManuscriptEditorState,
|
|
1985
|
+
dispatch?: Dispatch
|
|
1986
|
+
) => {
|
|
1987
|
+
const pluginState = searchReplaceKey.getState(state)
|
|
1988
|
+
const tr = state.tr.setMeta(searchReplaceKey, {
|
|
1989
|
+
activeAdvanced: !pluginState?.activeAdvanced,
|
|
1990
|
+
})
|
|
1991
|
+
dispatch && dispatch(tr)
|
|
1992
|
+
return true
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const createHeroImage = (attrs?: Attrs) =>
|
|
1996
|
+
schema.nodes.hero_image.create(
|
|
1997
|
+
{
|
|
1998
|
+
...attrs,
|
|
1999
|
+
id: generateNodeID(schema.nodes.hero_image),
|
|
2000
|
+
},
|
|
2001
|
+
[
|
|
2002
|
+
schema.nodes.figure.create(),
|
|
2003
|
+
schema.nodes.alt_text.create(),
|
|
2004
|
+
schema.nodes.long_desc.create(),
|
|
2005
|
+
]
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
export const insertHeroImage =
|
|
2009
|
+
() =>
|
|
2010
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
2011
|
+
const position = findInsertionPosition(schema.nodes.hero_image, state.doc)
|
|
2012
|
+
view?.focus()
|
|
2013
|
+
createBlock(schema.nodes.hero_image, position, state, dispatch)
|
|
2014
|
+
|
|
2015
|
+
return true
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
export const ignoreEnterInSubtitles = (state: ManuscriptEditorState) => {
|
|
2019
|
+
const { selection } = state
|
|
2020
|
+
|
|
2021
|
+
if (!isTextSelection(selection)) {
|
|
2022
|
+
return false
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// Check where the cursor is positioned
|
|
2026
|
+
const cursorParent = selection.$from.node()
|
|
2027
|
+
|
|
2028
|
+
if (
|
|
2029
|
+
cursorParent.type === state.schema.nodes.subtitle ||
|
|
2030
|
+
cursorParent.type === state.schema.nodes.subtitles
|
|
2031
|
+
) {
|
|
2032
|
+
return true // Prevent Enter in subtitle area
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
return false
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Command to exit editor and focus container
|
|
2039
|
+
export const exitEditorToContainer: EditorAction = (state, dispatch, view) => {
|
|
2040
|
+
const editorContainer = document.getElementById('editor')
|
|
2041
|
+
if (editorContainer && dispatch && view) {
|
|
2042
|
+
editorContainer.focus()
|
|
2043
|
+
const tr = view.state.tr.setMeta(persistentCursor, { on: true })
|
|
2044
|
+
view.dispatch(tr)
|
|
2045
|
+
return true
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
return false
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
export const copySelection = (
|
|
2052
|
+
state: ManuscriptEditorState,
|
|
2053
|
+
dispatch?: Dispatch,
|
|
2054
|
+
view?: EditorView
|
|
2055
|
+
) => {
|
|
2056
|
+
const { selection } = state
|
|
2057
|
+
const clipboard = navigator?.clipboard
|
|
2058
|
+
|
|
2059
|
+
if (selection.content().size && clipboard) {
|
|
2060
|
+
view &&
|
|
2061
|
+
(async () => {
|
|
2062
|
+
try {
|
|
2063
|
+
const { dom, text } = view.serializeForClipboard(selection.content())
|
|
2064
|
+
await clipboard.write([
|
|
2065
|
+
new ClipboardItem({
|
|
2066
|
+
'text/plain': new Blob([text], { type: 'text/plain' }),
|
|
2067
|
+
'text/html': new Blob([dom.innerHTML], { type: 'text/html' }),
|
|
2068
|
+
}),
|
|
2069
|
+
])
|
|
2070
|
+
} catch (e) {
|
|
2071
|
+
console.error('clipboard writer error:', e)
|
|
2072
|
+
}
|
|
2073
|
+
})()
|
|
2074
|
+
return true
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
return false
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
export const paste =
|
|
2081
|
+
(format: 'html' | 'text') =>
|
|
2082
|
+
(state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
|
|
2083
|
+
const clipboard = navigator?.clipboard
|
|
2084
|
+
|
|
2085
|
+
if (clipboard) {
|
|
2086
|
+
view &&
|
|
2087
|
+
(async () => {
|
|
2088
|
+
try {
|
|
2089
|
+
const items = await clipboard.read()
|
|
2090
|
+
const htmlItem = await items.find(({ types }) =>
|
|
2091
|
+
types.includes('text/html')
|
|
2092
|
+
)
|
|
2093
|
+
if (format === 'html' && htmlItem) {
|
|
2094
|
+
const htmlBlob = await htmlItem.getType('text/html')
|
|
2095
|
+
const html = await htmlBlob.text()
|
|
2096
|
+
view.pasteHTML(html)
|
|
2097
|
+
} else {
|
|
2098
|
+
const text = await clipboard.readText()
|
|
2099
|
+
view.pasteText(text)
|
|
2100
|
+
}
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
console.error('clipboard reader error:', e)
|
|
2103
|
+
}
|
|
2104
|
+
})()
|
|
2105
|
+
return true
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
return false
|
|
2109
|
+
}
|