diff --git a/editors/editoria/package.json b/editors/editoria/package.json index df7329a24eb0b2d1d99ed0ffc4a3f1ef9759ee5d..dc5d6b1ebabae4f0b74a88194086c68c0d831d1b 100644 --- a/editors/editoria/package.json +++ b/editors/editoria/package.json @@ -16,7 +16,8 @@ "wax-prosemirror-services": "^0.0.20", "fontsource-merriweather": "^3.0.9", "typeface-fira-sans-condensed": "^0.0.54", - "typeface-vollkorn": "^0.0.54" + "typeface-vollkorn": "^0.0.54", + "typeface-inter": "^1.1.13" }, "scripts": { "start": "react-app-rewired start", diff --git a/editors/editoria/src/config/config.js b/editors/editoria/src/config/config.js index a10a5e399d5f0babefb052f069c8a1a7db5b723f..9e34bee28f37e71ae99d9a2e6c6ca05835921e94 100644 --- a/editors/editoria/src/config/config.js +++ b/editors/editoria/src/config/config.js @@ -26,6 +26,8 @@ import { TrackChangeToolGroupService, DisplayTextToolGroupService, MathService, + FindAndReplaceService, + TrackingAndEditingToolGroupService, } from 'wax-prosemirror-services'; import { WaxSelectionPlugin } from 'wax-prosemirror-plugins'; @@ -51,7 +53,7 @@ export default { 'Images', 'CodeBlock', 'Tables', - 'TrackChange', + 'TrackingAndEditing', ], }, { @@ -99,5 +101,7 @@ export default { new TrackChangeToolGroupService(), new DisplayTextToolGroupService(), new MathService(), + new FindAndReplaceService(), + new TrackingAndEditingToolGroupService(), ], }; diff --git a/editors/editoria/src/layout/EditorElements.js b/editors/editoria/src/layout/EditorElements.js index accd9cbc2b66e2a75b1228cf4811ba4cd0b17588..66b1215c748d31adba1276cf6ec6e212b44a96a9 100644 --- a/editors/editoria/src/layout/EditorElements.js +++ b/editors/editoria/src/layout/EditorElements.js @@ -14,6 +14,7 @@ export default css` .ProseMirror { background: white; counter-reset: footnote; + line-height: 1.6; ${fontWriting} p::selection, @@ -45,7 +46,7 @@ export default css` } .ProseMirror footnote { - font-size: 0; + font-variant-numeric: lining-nums proportional-nums; display: inline-block; text-align: center; width: 17px; @@ -256,6 +257,10 @@ export default css` } } + span.search-result { + background: #bee594; + } + /* == Math Nodes ======================================== */ .math-node { diff --git a/editors/editoria/src/layout/EditoriaLayout.js b/editors/editoria/src/layout/EditoriaLayout.js index 1decde8c2b0be137236efd8659fb5d461cf10733..4668f50752ec28a48bdb240960e9e28ef49a27da 100644 --- a/editors/editoria/src/layout/EditoriaLayout.js +++ b/editors/editoria/src/layout/EditoriaLayout.js @@ -1,10 +1,9 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import styled, { css, ThemeProvider } from 'styled-components'; import PanelGroup from 'react-panelgroup'; import { DocumentHelpers } from 'wax-prosemirror-utilities'; import { WaxContext, ComponentPlugin } from 'wax-prosemirror-core'; import { grid, th } from '@pubsweet/ui-toolkit'; - import { cokoTheme } from '../theme'; import EditorElements from './EditorElements'; @@ -13,7 +12,7 @@ import '~../../katex/dist/katex.min.css'; const divider = css` .panelGroup { - background: ${th('colorBackgroundHue')}; + background: #fff; } .divider { > div { @@ -60,6 +59,14 @@ const TopMenu = styled.div` border-right: ${th('borderWidth')} ${th('borderStyle')} ${th('colorFurniture')}; } + + > div:last-child { + margin-left: auto; + margin-right: 10px; + } + > div[data-name="Tables"]{ + border-right: none; + } `; const SideMenu = styled.div` @@ -103,7 +110,6 @@ const CommentsContainer = styled.div` `; const CommentsContainerNotes = styled.div` - background: ${th('colorBackgroundHue')}; display: flex; flex-direction: column; width: 35%; @@ -126,6 +132,7 @@ const NotesAreaContainer = styled.div` `; const NotesContainer = styled.div` + box-shadow: 0 0 8px #ecedf1; counter-reset: footnote-view; display: flex; flex-direction: column; @@ -142,7 +149,7 @@ const onResizeEnd = arr => { notesHeight = arr[1].size; }; -const hasNotes = main => { +const getNotes = main => { const notes = DocumentHelpers.findChildrenByType( main.state.doc, main.state.schema.nodes.footnote, @@ -152,7 +159,6 @@ const hasNotes = main => { }; const LeftSideBar = ComponentPlugin('leftSideBar'); -// const RightSideBar = ComponentPlugin('rightSideBar'); const TopBar = ComponentPlugin('topBar'); const NotesArea = ComponentPlugin('notesArea'); const RightArea = ComponentPlugin('rightArea'); @@ -163,8 +169,21 @@ const EditoriaLayout = ({ editor }) => { view: { main }, } = useContext(WaxContext); - const notes = main && hasNotes(main); - const showNotes = notes && !!notes.length && notes.length > 0; + const notes = main && getNotes(main); + const areNotes = notes && !!notes.length && notes.length > 0; + + const [hasNotes, setHasNotes] = useState(areNotes); + + const showNotes = () => { + setHasNotes(areNotes); + }; + + const delayedShowedNotes = useCallback( + setTimeout(() => showNotes(), 100), + [], + ); + + useEffect(() => {}, [delayedShowedNotes]); return ( <ThemeProvider theme={cokoTheme}> @@ -183,7 +202,7 @@ const EditoriaLayout = ({ editor }) => { direction="column" panelWidths={[ { size: surfaceHeight, resize: 'stretch' }, - { size: notesHeight, resize: 'stretch' }, + { size: notesHeight, resize: 'resize' }, ]} onResizeEnd={onResizeEnd} > @@ -194,10 +213,10 @@ const EditoriaLayout = ({ editor }) => { </CommentsContainer> </WaxSurfaceScroll> - {showNotes && ( + {hasNotes && ( <NotesAreaContainer> <NotesContainer id="notes-container"> - <NotesArea /> + <NotesArea view={main} /> </NotesContainer> <CommentsContainerNotes> <RightArea area="notes" /> @@ -207,7 +226,6 @@ const EditoriaLayout = ({ editor }) => { </PanelGroup> </EditorArea> </Main> - <WaxOverlays /> </Wrapper> </ThemeProvider> diff --git a/editors/editoria/src/layout/EditoriaMobileLayout.js b/editors/editoria/src/layout/EditoriaMobileLayout.js index 453c18cec642be3a03621f2e9b110ce2b76203a9..5a51b110d0cfee6101396e33963689284350ef4b 100644 --- a/editors/editoria/src/layout/EditoriaMobileLayout.js +++ b/editors/editoria/src/layout/EditoriaMobileLayout.js @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import styled, { css, ThemeProvider } from 'styled-components'; import PanelGroup from 'react-panelgroup'; import { DocumentHelpers } from 'wax-prosemirror-utilities'; @@ -13,7 +13,7 @@ import '~../../katex/dist/katex.min.css'; const divider = css` .panelGroup { - background: ${th('colorBackgroundHue')}; + background: #fff; } .divider { > div { @@ -67,7 +67,7 @@ const TopMenu = styled.div` } > div:not(:last-child) { border-right: ${th('borderWidth')} ${th('borderStyle')} - ${th('colorFurniture')}; + ${th('colorBorder')}; } button: { height: 20px; @@ -117,13 +117,13 @@ const CommentsContainer = styled.div` `; const CommentsContainerNotes = styled.div` - background: ${th('colorBackgroundHue')}; display: flex; flex-direction: column; height: 100%; `; const NotesAreaContainer = styled.div` + box-shadow: 0 0 8px #ecedf1; background: #fff; display: flex; flex-direction: row; @@ -156,7 +156,7 @@ const onResizeEnd = arr => { notesHeight = arr[1].size; }; -const hasNotes = main => { +const getNotes = main => { const notes = DocumentHelpers.findChildrenByType( main.state.doc, main.state.schema.nodes.footnote, @@ -175,8 +175,21 @@ const EditoriaLayout = ({ editor }) => { view: { main }, } = useContext(WaxContext); - const notes = main && hasNotes(main); - const showNotes = notes && !!notes.length && notes.length > 0; + const notes = main && getNotes(main); + const areNotes = notes && !!notes.length && notes.length > 0; + + const [hasNotes, setHasNotes] = useState(areNotes); + + const showNotes = () => { + setHasNotes(areNotes); + }; + + const delayedShowedNotes = useCallback( + setTimeout(() => showNotes(), 100), + [], + ); + + useEffect(() => {}, [delayedShowedNotes]); return ( <ThemeProvider theme={cokoTheme}> @@ -191,7 +204,7 @@ const EditoriaLayout = ({ editor }) => { direction="column" panelWidths={[ { size: surfaceHeight, resize: 'stretch' }, - { size: notesHeight, resize: 'stretch' }, + { size: notesHeight, resize: 'resize' }, ]} onResizeEnd={onResizeEnd} > @@ -202,10 +215,10 @@ const EditoriaLayout = ({ editor }) => { </CommentsContainer> </WaxSurfaceScroll> - {showNotes && ( + {hasNotes && ( <NotesAreaContainer> <NotesContainer id="notes-container"> - <NotesArea /> + <NotesArea view={main} /> </NotesContainer> <CommentsContainerNotes> <RightArea area="notes" /> diff --git a/editors/editoria/src/theme/theme.js b/editors/editoria/src/theme/theme.js index 54c45d7069224b9529dd426c47eac26b8d263888..011ea8916f75580b4474d3ab63d6d5c8e10fdaa0 100644 --- a/editors/editoria/src/theme/theme.js +++ b/editors/editoria/src/theme/theme.js @@ -2,26 +2,28 @@ import 'typeface-fira-sans-condensed'; import 'fontsource-merriweather'; import 'typeface-vollkorn'; +import 'typeface-inter'; import { css } from 'styled-components'; const cokoTheme = { /* Colors */ colorBackground: 'white', - colorPrimary: '#808080', + colorPrimary: '#525E76', colorSecondary: '#E7E7E7', colorFurniture: '#CCC', - colorBorder: '#AAA', - colorBackgroundHue: '#F7F7F7', + colorBorder: '#EBEBF0', + colorBackgroundHue: '#F1F5FF', + colorBackgroundTabs: '#e1ebff', colorSuccess: '#008800', colorError: 'indianred', colorText: '#111', colorTextReverse: '#FFF', colorTextPlaceholder: '#595959', colorWarning: '#ffc107', - colorBackgroundToolBar: '#D9D9D9', - colorSelection: '#D3D3D3', - + colorBackgroundToolBar: '#fff', + colorSelection: '#C5D7FE', + colorBackgroundButton: '#0042C7', /* Text variables */ // fonts @@ -29,6 +31,7 @@ const cokoTheme = { fontHeading: 'Fira Sans Condensed', fontReading: 'Vollkorn', fontWriting: 'Merriweather', + fontTools: 'Inter', // font sizes fontSizeBase: '14px', diff --git a/stories/buttons/Icon.stories.js b/stories/buttons/Icon.stories.js index f2eb45da2db42136b8219b184efb2e921df25ae5..0be84f3b449ea475f569b83bd4680560f24174d9 100644 --- a/stories/buttons/Icon.stories.js +++ b/stories/buttons/Icon.stories.js @@ -1,6 +1,6 @@ import React from 'react'; -import Icon from '../../wax-prosemirror-components/src/ui/buttons/Icon'; +import Icon from '../../wax-prosemirror-components/src/helpers/Icon'; export const Base = () => <Icon name="commentBubble" />; diff --git a/wax-prosemirror-components/index.js b/wax-prosemirror-components/index.js index 0da2eedf1adc1534144ec32ffa791cc73a9713d7..7c670937663bc079f0d8f1eb51a1d7e7b3f4e4b0 100644 --- a/wax-prosemirror-components/index.js +++ b/wax-prosemirror-components/index.js @@ -6,18 +6,12 @@ export { default as ImageUpload } from './src/components/ImageUpload'; export { default as LeftMenuTitle } from './src/components/LeftMenuTitle'; export { default as ToolGroupComponent } from './src/components/ToolGroupComponent'; export { default as ToolGroups } from './src/components/ToolGroups'; - export { default as NoteEditorContainer } from './src/components/notes/NoteEditorContainer'; - export { default as LinkComponent } from './src/components/link/LinkComponent'; - export { default as CommentBubbleComponent } from './src/components/comments/CommentBubbleComponent'; - export { default as RightArea } from './src/components/rightArea/RightArea'; - export { default as TrackChangeEnable } from './src/components/trackChanges/TrackChangeEnable'; - export { default as CreateTable } from './src/components/tables/CreateTable'; - export { default as Tabs } from './src/ui/tabs/Tabs'; export { default as BlockLevelTools } from './src/ui/tabs/BlockLevelTools'; +export { default as FindAndReplaceTool } from './src/components/findAndReplace/FindAndReplaceTool'; diff --git a/wax-prosemirror-components/package.json b/wax-prosemirror-components/package.json index d6d6d7007b6dc094c2306b9f28ce0f991939e621..2ca455a0635a71ea44c457138706350fb0591725 100644 --- a/wax-prosemirror-components/package.json +++ b/wax-prosemirror-components/package.json @@ -4,7 +4,7 @@ "version": "0.0.20", "description": "Wax prosemirror UI components", "license": "MIT", - "main": "index.js", + "main": "dist/index.js", "files": [ "dist" ], diff --git a/wax-prosemirror-components/src/components/Button.js b/wax-prosemirror-components/src/components/Button.js index fa2e0d6efdf838a56b2adf9ed84dd48d5d8b8276..4edf40a850e80af769f651662087cfc29de8652a 100644 --- a/wax-prosemirror-components/src/components/Button.js +++ b/wax-prosemirror-components/src/components/Button.js @@ -10,6 +10,7 @@ const Button = ({ view = {}, item }) => { const { view: { main }, activeViewId, + activeView, } = useContext(WaxContext); if (onlyOnMain) view = main; @@ -21,10 +22,9 @@ const Button = ({ view = {}, item }) => { run(editorState, dispatch); }; - const isActive = active && active(state, activeViewId); + const isActive = active(state, activeViewId) && select(state, activeViewId); - const isDisabled = - enable && !enable(state) && !(select && select(state, activeViewId)); + const isDisabled = !select(state, activeViewId, activeView); const MenuButtonComponent = useMemo( () => ( diff --git a/wax-prosemirror-components/src/components/ImageUpload.js b/wax-prosemirror-components/src/components/ImageUpload.js index b94c0a7e544cd9a2c0c8c322de878770755537f4..8a0be50407b680f36e518ff3d24c7095991b50f9 100644 --- a/wax-prosemirror-components/src/components/ImageUpload.js +++ b/wax-prosemirror-components/src/components/ImageUpload.js @@ -17,13 +17,15 @@ const ImageUpload = ({ item, fileUpload, view }) => { const inputRef = useRef(null); const handleMouseDown = () => inputRef.current.click(); + const isDisabled = !item.select(view.state, activeViewId); + const ImageUploadComponent = useMemo( () => ( <Wrapper> <label htmlFor="file-upload"> <MenuButton active={false} - disabled={!(item.select && item.select(view.state, activeViewId))} + disabled={isDisabled} iconName={item.icon} onMouseDown={handleMouseDown} title="Upload Image" @@ -38,7 +40,7 @@ const ImageUpload = ({ item, fileUpload, view }) => { </label> </Wrapper> ), - [], + [isDisabled], ); return ImageUploadComponent; diff --git a/wax-prosemirror-components/src/components/TableDropDown.js b/wax-prosemirror-components/src/components/TableDropDown.js index 1e7c6a1108ed7ff7c31a0191ef3613864f46402c..2fa2afc9b439aa75ace7b66b7fb51fdd869a3f23 100644 --- a/wax-prosemirror-components/src/components/TableDropDown.js +++ b/wax-prosemirror-components/src/components/TableDropDown.js @@ -1,7 +1,8 @@ /* eslint react/prop-types: 0 */ -import React from 'react'; +import React, { useMemo, useContext } from 'react'; import styled from 'styled-components'; import * as tablesFn from 'prosemirror-tables'; +import { WaxContext } from 'wax-prosemirror-core'; import Dropdown from 'react-dropdown'; import 'react-dropdown/style.css'; @@ -44,16 +45,24 @@ const dropDownOptions = [ ]; const TableDropDown = ({ view: { dispatch, state }, item }) => { - return ( - <DropdownStyled - options={dropDownOptions} - onChange={option => { - item.run(state, dispatch, tablesFn[option.value]); - }} - placeholder="Table Options" - select={item.select && item.select(state)} - /> + const { activeView } = useContext(WaxContext); + + const isDisabled = item.select(activeView.state); + const TableDropDownComponent = useMemo( + () => ( + <DropdownStyled + options={dropDownOptions} + onChange={option => { + item.run(state, dispatch, tablesFn[option.value]); + }} + placeholder="Table Options" + select={isDisabled} + /> + ), + [isDisabled], ); + + return TableDropDownComponent; }; export default TableDropDown; diff --git a/wax-prosemirror-components/src/components/ToolGroupComponent.js b/wax-prosemirror-components/src/components/ToolGroupComponent.js index 6ab71762b38941a1852d6d3461a88ad90a00c694..361f3a49450a3e2620f0cb5364cebe751bb40bc7 100644 --- a/wax-prosemirror-components/src/components/ToolGroupComponent.js +++ b/wax-prosemirror-components/src/components/ToolGroupComponent.js @@ -11,7 +11,7 @@ const Wrapper = styled.div` > button, > div { - margin: 0 2px; + margin: 0 5px; } `; diff --git a/wax-prosemirror-components/src/components/comments/ConnectedComment.js b/wax-prosemirror-components/src/components/comments/ConnectedComment.js index 6123e7382bb9dc8761fee39e671d82dfdff61ee3..7360c85b9be17eefd8830c1a1cc172448d6cd860 100644 --- a/wax-prosemirror-components/src/components/comments/ConnectedComment.js +++ b/wax-prosemirror-components/src/components/comments/ConnectedComment.js @@ -30,11 +30,14 @@ export default ({ comment, top, commentId, recalculateTops }) => { const { state, dispatch } = activeView; const viewId = comment.attrs.viewid; + let allCommentsWithSameId = []; - const allCommentsWithSameId = DocumentHelpers.findAllMarksWithSameId( - view[viewId].state, - comment, - ); + if (view[viewId]) { + allCommentsWithSameId = DocumentHelpers.findAllMarksWithSameId( + view[viewId].state, + comment, + ); + } const commentMark = state.schema.marks.comment; @@ -53,13 +56,6 @@ export default ({ comment, top, commentId, recalculateTops }) => { } const onClickPost = content => { - // TODO find out why on enter comment posts twice. - if ( - comment.attrs.conversation.length !== 0 && - last(comment.attrs.conversation).content === content - ) - return; - const { tr } = state; const obj = { @@ -128,14 +124,16 @@ export default ({ comment, top, commentId, recalculateTops }) => { const onTextAreaBlur = (content, isNewComment) => { // TODO Save into local storage - if (content !== '') { - onClickPost(content); - } - - if (content === '' && isNewComment) { - onClickResolve(); - activeView.focus(); - } + // if (content !== '') { + // onClickPost(content); + // } + + setTimeout(() => { + if (comment.attrs.conversation.length === 0 && isNewComment) { + onClickResolve(); + activeView.focus(); + } + }, 200); }; const MemorizedComponent = useMemo( diff --git a/wax-prosemirror-components/src/components/findAndReplace/ExandedFindAndReplaceComponent.js b/wax-prosemirror-components/src/components/findAndReplace/ExandedFindAndReplaceComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..45300451e697cb164e80fca2b0627ea4b6fcf630 --- /dev/null +++ b/wax-prosemirror-components/src/components/findAndReplace/ExandedFindAndReplaceComponent.js @@ -0,0 +1,266 @@ +/* eslint react/prop-types: 0 */ +import React, { + useState, + useRef, + useContext, + useCallback, + useEffect, +} from 'react'; +import { each, eachRight, debounce } from 'lodash'; +import { WaxContext } from 'wax-prosemirror-core'; +import styled from 'styled-components'; +import { grid, th } from '@pubsweet/ui-toolkit'; +import Icon from '../../helpers/Icon'; +import CheckBox from '../../ui/inputs/CheckBox'; +import helpers from './helpers'; + +const Wrapper = styled.div` + font-size: 15px; + width: 400px; + height: 300px; + background: #fff; + font-family: ${th('fontTools')}; + box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px 0px, + rgba(9, 30, 66, 0.31) 0px 0px 1px 0px; + transform-origin: 50% 50% 0px; + padding: ${grid(2)}; +`; + +const FindTitle = styled.span` + font-size: 16px; + color: #4b5871; +`; + +const TitleContainer = styled.div` + display: flex; + flex-direction: row; +`; + +const InputLabel = styled.div` + padding: ${grid(2)} ${grid(0)} ${grid(2)} ${grid(0)}; + font-size: 16px; + color: #4b5871; +`; + +const SearchInputWrapper = styled.div` + input { + padding: ${grid(1)} ${grid(10)} ${grid(1)} ${grid(1)}; + width: 89%; + } +`; + +const CounterInput = styled.span` + position: absolute; + right: 14px; + top: 72px; + z-index: 1; + font-size: 12px; + -webkit-text-fill-color: rgba(27, 43, 75, 0.28); +`; + +const StyledIcon = styled(Icon)` + height: 24px; + width: 24px; + cursor: pointer; +`; + +const CloseWrapper = styled.div` + margin-left: auto; +`; + +const FindReplaceInput = styled.input` + font-size: 15px; + font-weight: 400; + border-radius: 2px; + border: none; + padding: ${grid(1)}; + width: 98%; + box-shadow: inset 0 0 0 1px rgba(27, 43, 75, 0.28); + ::placeholder { + color: #d8dae0; + } + &:focus { + outline: none; + } +`; + +const CheckBoxWrapper = styled.div` + margin-top: 20px; +`; + +const ControlContainer = styled.div` + display: flex; + flex-direction: row; + margin-top: 70px; +`; + +const ButtonReplace = styled.button` + background: ${th('colorBackgroundButton')}; + margin-right: 20px; + border: 1px solid ${th('colorBackgroundButton')}; + color: white; + width: 100px; + height: 42px; + cursor: pointer; +`; + +const ButtonReplaceAll = styled.button` + background: white; + border: 1px solid ${th('colorBackgroundButton')}; + margin-right: 10px; + color: ${th('colorBackgroundButton')}; + width: 100px; + height: 42px; + cursor: pointer; +`; + +const PreviousNextContainer = styled.div` + margin: 3px 0 0 auto; + + svg { + padding: ${grid(2)}; + + &:hover { + background: #f1f5ff; + } + } + + svg:first-child { + margin-right: 20px; + } +`; + +const PreviousNextButton = styled.span` + &:focus { + outline: none; + } +`; + +const ExandedFindAndReplaceComponent = ({ close, nonExpandedText }) => { + const { app, view } = useContext(WaxContext); + const searchRef = useRef(null); + const replaceRef = useRef(null); + const [searchValue, setSearchValue] = useState(nonExpandedText); + const [replaceValue, setReplaceValue] = useState(''); + const [counterText, setCounterText] = useState('0 of 0'); + const findAndReplacePlugin = app.PmPlugins.get('findAndReplacePlugin'); + + const allStates = []; + + each(view, (singleView, viewId) => { + allStates.push(singleView.state); + }); + + const delayedSearch = useCallback( + debounce(() => searchDocument(), 300), + [searchValue], + ); + + const onChangeSearchInput = () => { + setSearchValue(searchRef.current.value); + }; + + useEffect(() => { + delayedSearch(); + }, [searchValue, delayedSearch, JSON.stringify(allStates)]); + + const searchDocument = () => { + setCounterText('0 of 0'); + let counter = 0; + findAndReplacePlugin.props.setSearchText(searchValue); + counter = helpers.getMatchesByView(view, searchValue); + + if (counter > 0) setCounterText(`1 of ${counter}`); + + if (searchRef.current === document.activeElement) { + each(view, (singleView, viewId) => { + singleView.dispatch(singleView.state.tr); + }); + } + }; + + const onChangeReplaceInput = () => { + setReplaceValue(replaceRef.current.value); + }; + + const replace = () => { + // const { from, to } = results[0]; + // dispatch(state.tr.insertText(replace, from, to)); + }; + const replaceAll = () => { + each(view, (singleView, viewId) => { + const results = helpers.findMatches(singleView.state.doc, searchValue); + const { + state: { tr }, + } = singleView; + + eachRight(results, result => { + tr.insertText(replaceValue, result.from, result.to); + }); + singleView.dispatch(tr); + }); + }; + + const closeFind = () => { + findAndReplacePlugin.props.setSearchText(''); + each(view, (singleView, viewId) => { + singleView.dispatch(singleView.state.tr); + }); + close(); + }; + + const findNext = () => { + console.log('next'); + }; + + const findPrevious = () => { + console.log('previous'); + }; + + return ( + <Wrapper> + <TitleContainer> + <FindTitle> Find & Replace </FindTitle> + <CloseWrapper onClick={closeFind}> + <StyledIcon name="close" /> + </CloseWrapper> + </TitleContainer> + <InputLabel>Find</InputLabel> + + <SearchInputWrapper> + <FindReplaceInput + type="text" + ref={searchRef} + placeholder="Something is this doc" + value={searchValue} + onChange={onChangeSearchInput} + /> + <CounterInput> {counterText} </CounterInput> + </SearchInputWrapper> + <InputLabel>Replace with</InputLabel> + <FindReplaceInput + type="text" + ref={replaceRef} + placeholder="Replace text" + onChange={onChangeReplaceInput} + /> + <CheckBoxWrapper> + <CheckBox name="case-sensitive" label="Case Sensitive" /> + </CheckBoxWrapper> + <ControlContainer> + <ButtonReplace onClick={replace}>Replace</ButtonReplace> + <ButtonReplaceAll onClick={replaceAll}>Replace All</ButtonReplaceAll> + <PreviousNextContainer> + <PreviousNextButton onClick={findPrevious} role="button" tabIndex="0"> + <StyledIcon name="navigatePrevious" /> + </PreviousNextButton> + <PreviousNextButton onClick={findNext} role="button" tabIndex="0"> + <StyledIcon name="navigateNext" /> + </PreviousNextButton> + </PreviousNextContainer> + </ControlContainer> + </Wrapper> + ); +}; + +export default ExandedFindAndReplaceComponent; diff --git a/wax-prosemirror-components/src/components/findAndReplace/FindAndReplaceComponent.js b/wax-prosemirror-components/src/components/findAndReplace/FindAndReplaceComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..2a36e0ed115c6ab4d094510d05345d29e8e5cb4e --- /dev/null +++ b/wax-prosemirror-components/src/components/findAndReplace/FindAndReplaceComponent.js @@ -0,0 +1,31 @@ +/* eslint react/prop-types: 0 */ +import React, { useState } from 'react'; +import FindComponent from './FindComponent'; +import ExandedFindAndReplaceComponent from './ExandedFindAndReplaceComponent'; + +const FindAndReplaceComponent = ({ close }) => { + const [isExpanded, setExpanded] = useState(false); + const [nonExpandedText, SetnonExpandedText] = useState(''); + const expand = () => { + setExpanded(true); + }; + + const getNonExpandedText = searcString => { + SetnonExpandedText(searcString); + }; + + return isExpanded ? ( + <ExandedFindAndReplaceComponent + close={close} + nonExpandedText={nonExpandedText} + /> + ) : ( + <FindComponent + close={close} + expand={expand} + setPreviousSearcValue={getNonExpandedText} + /> + ); +}; + +export default FindAndReplaceComponent; diff --git a/wax-prosemirror-components/src/components/findAndReplace/FindAndReplaceTool.js b/wax-prosemirror-components/src/components/findAndReplace/FindAndReplaceTool.js new file mode 100644 index 0000000000000000000000000000000000000000..9bf82295036dc2b48daeaab28ab5e7c45c0890d6 --- /dev/null +++ b/wax-prosemirror-components/src/components/findAndReplace/FindAndReplaceTool.js @@ -0,0 +1,76 @@ +import React, { useRef, useMemo, useState, useLayoutEffect } from 'react'; + +import styled from 'styled-components'; +import { grid } from '@pubsweet/ui-toolkit'; + +import MenuButton from '../../ui/buttons/MenuButton'; +import FindAndReplaceComponent from './FindAndReplaceComponent'; + +const Wrapper = styled.div` + font-size: 0; + position: relative; + z-index: 2; +`; + +const DropWrapper = styled.div` + margin-top: ${grid(1)}; + position: absolute; + background: white; + top: 32px; +`; + +const FindAndReplaceTool = ({ view = {}, item }) => { + const { state } = view; + const { enable, icon, run, select, title } = item; + const dropElement = useRef(); + const [isOpen, setIsOpen] = useState(false); + + // const isDisabled = + // enable && !enable(state) && !(select && select(state, activeViewId)); + // + + let styles = { right: '-205px' }; + const [style, setStyle] = useState(styles); + + useLayoutEffect(() => { + setStyle(styles); + if (!dropElement.current) return; + const { right } = dropElement.current.getBoundingClientRect(); + if (right > window.window.innerWidth) { + const newRight = -205 + (right - window.window.innerWidth) + 15; + styles = { right: `${newRight}px` }; + setStyle(styles); + } + }, [isOpen]); + + const MemorizedDropdown = useMemo( + () => ( + <Wrapper> + <MenuButton + active={isOpen} + disabled={false} + iconName={icon} + onMouseDown={() => { + setIsOpen(!isOpen); + }} + title={title} + /> + + {isOpen && ( + <DropWrapper style={style} ref={dropElement}> + <FindAndReplaceComponent + close={() => { + setIsOpen(false); + }} + /> + </DropWrapper> + )} + </Wrapper> + ), + [isOpen, style], + ); + + return MemorizedDropdown; +}; + +export default FindAndReplaceTool; diff --git a/wax-prosemirror-components/src/components/findAndReplace/FindComponent.js b/wax-prosemirror-components/src/components/findAndReplace/FindComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..cefd02958236adc544cbe54f84bb413fd5e76cdb --- /dev/null +++ b/wax-prosemirror-components/src/components/findAndReplace/FindComponent.js @@ -0,0 +1,181 @@ +/* eslint react/prop-types: 0 */ + +import React, { + useState, + useRef, + useContext, + useCallback, + useEffect, +} from 'react'; +import { debounce, each, eachRight } from 'lodash'; +import styled from 'styled-components'; +import { grid } from '@pubsweet/ui-toolkit'; +import { WaxContext } from 'wax-prosemirror-core'; +import Icon from '../../helpers/Icon'; +import helpers from './helpers'; + +const Wrapper = styled.div` + width: 400px; + background: #fff; + border-radius: 1.03093% / 8%; + box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px 0px, + rgba(9, 30, 66, 0.31) 0px 0px 1px 0px; + transform-origin: 50% 50% 0px; + padding: ${grid(2)}; +`; + +const SingleRow = styled.div` + display: flex; + flex-direction: row; +`; + +const SearchInputWrapper = styled.div` + width: 75%; +`; + +const SearchInput = styled.input` + font-size: 15px; + font-weight: 400; + border-radius: 2px; + border: none; + padding: ${grid(1)} ${grid(10)} ${grid(1)} ${grid(1)}; + width: 85%; + box-shadow: inset 0 0 0 1px rgba(27, 43, 75, 0.28); + ::placeholder { + color: #d8dae0; + } + &:focus { + outline: none; + } +`; + +const CounterInput = styled.span` + position: absolute; + right: 115px; + top: 13px; + z-index: 1; + font-size: 12px; + -webkit-text-fill-color: rgba(27, 43, 75, 0.28); +`; + +const StyledIcon = styled(Icon)` + height: 24px; + width: 24px; + cursor: pointer; +`; + +const CloseWrapper = styled.div` + border-left: 1px solid #e0e2e7; + margin-left: 1%; +`; + +const PreviousNextButton = styled.span` + &:focus { + outline: none; + } +`; + +const ExpandedWrapper = styled.div``; + +const FindComponent = ({ close, expand, setPreviousSearcValue }) => { + const { app, view } = useContext(WaxContext); + + const searchRef = useRef(null); + const [searchValue, setSearchValue] = useState(''); + const [counterText, setCounterText] = useState('0 of 0'); + const findAndReplacePlugin = app.PmPlugins.get('findAndReplacePlugin'); + const [isFirstRun, setFirstRun] = useState(true); + const allStates = []; + + each(view, (singleView, viewId) => { + allStates.push(singleView.state); + }); + + const delayedSearch = useCallback( + debounce(() => searchDocument(), 300), + [searchValue], + ); + + const onChange = () => { + setSearchValue(searchRef.current.value); + }; + + useEffect(() => { + delayedSearch(); + if (isFirstRun) { + setTimeout(() => { + searchRef.current.focus(); + setFirstRun(false); + }); + } + }, [searchValue, delayedSearch, JSON.stringify(allStates)]); + + const searchDocument = () => { + setCounterText('0 of 0'); + let counter = 0; + findAndReplacePlugin.props.setSearchText(searchValue); + counter = helpers.getMatchesByView(view, searchValue); + + if (counter > 0) setCounterText(`1 of ${counter}`); + + if (searchRef.current === document.activeElement) { + eachRight(view, (singleView, viewId) => { + singleView.dispatch(singleView.state.tr); + }); + } + }; + + const closeFind = () => { + findAndReplacePlugin.props.setSearchText(''); + each(view, (singleView, viewId) => { + singleView.dispatch(singleView.state.tr); + }); + close(); + }; + + const showExpanded = () => { + expand(); + setPreviousSearcValue(searchValue); + }; + + const findNext = () => { + console.log('next'); + }; + + const findPrevious = () => { + console.log('previous'); + }; + + return ( + <Wrapper> + <SingleRow> + <SearchInputWrapper> + <SearchInput + ref={searchRef} + type="text" + placeholder="Find" + value={searchValue} + onChange={onChange} + /> + <CounterInput> {counterText} </CounterInput> + </SearchInputWrapper> + <PreviousNextButton onClick={findPrevious} role="button" tabIndex="0"> + <StyledIcon name="navigatePrevious" /> + </PreviousNextButton> + <PreviousNextButton onClick={findNext} role="button" tabIndex="0"> + <StyledIcon name="navigateNext" /> + </PreviousNextButton> + + <ExpandedWrapper onClick={showExpanded}> + <StyledIcon name="more" /> + </ExpandedWrapper> + + <CloseWrapper onClick={closeFind}> + <StyledIcon name="close" /> + </CloseWrapper> + </SingleRow> + </Wrapper> + ); +}; + +export default FindComponent; diff --git a/wax-prosemirror-components/src/components/findAndReplace/helpers.js b/wax-prosemirror-components/src/components/findAndReplace/helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..12602b236745e7b3e7e3f006681aeb76042e8cb2 --- /dev/null +++ b/wax-prosemirror-components/src/components/findAndReplace/helpers.js @@ -0,0 +1,64 @@ +import { each, eachRight } from 'lodash'; + +const findMatches = (doc, searchValue) => { + const allNodes = []; + + doc.descendants((node, pos) => { + allNodes.push({ node, pos }); + }); + + eachRight(allNodes, (node, index) => { + if (node.node.type.name === 'footnote') { + allNodes.splice(index + 1, node.node.childCount); + } + }); + + const results = []; + const mergedTextNodes = []; + let index = 0; + + allNodes.forEach((node, i) => { + if (node.node.isText) { + if (mergedTextNodes[index]) { + mergedTextNodes[index] = { + text: mergedTextNodes[index].text + node.node.text, + pos: mergedTextNodes[index].pos, + }; + } else { + mergedTextNodes[index] = { + text: node.node.text, + pos: node.pos, + }; + } + } else { + index += 1; + } + }); + mergedTextNodes.forEach(({ text, pos }) => { + const search = RegExp(searchValue, 'gui'); + let m; + // eslint-disable-next-line no-cond-assign + while ((m = search.exec(text))) { + if (m[0] === '') { + break; + } + + results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }); + } + }); + return results; +}; + +const getMatchesByView = (views, searchValue) => { + let allResults = 0; + each(views, (singleView, viewId) => { + const results = findMatches(singleView.state.doc, searchValue); + allResults += results.length; + }); + return allResults; +}; + +export default { findMatches, getMatchesByView }; diff --git a/wax-prosemirror-components/src/components/notes/NoteEditorContainer.js b/wax-prosemirror-components/src/components/notes/NoteEditorContainer.js index 8e08ea86c095800921f7b00b8ebd39c914935fc6..cd20ef692c4964130ae99370700208021eb534c0 100644 --- a/wax-prosemirror-components/src/components/notes/NoteEditorContainer.js +++ b/wax-prosemirror-components/src/components/notes/NoteEditorContainer.js @@ -1,5 +1,5 @@ import React from 'react'; -import { grid } from '@pubsweet/ui-toolkit'; +import { grid, th } from '@pubsweet/ui-toolkit'; import styled from 'styled-components'; import NoteNumber from './NoteNumber'; @@ -23,7 +23,7 @@ const NoteStyled = styled.div` height: auto; margin-top: 10px; height: 100%; - border-bottom: 1px solid black; + box-shadow: 0 2px 2px -2px ${th('colorPrimary')}; &:focus { outline: none; diff --git a/wax-prosemirror-components/src/components/rightArea/BoxList.js b/wax-prosemirror-components/src/components/rightArea/BoxList.js index 951621320c4ab0d6b90b10d1e8aef1d0da81e2b0..aa00fcc27cd25f85e0622eee4b2facba5f49b5ff 100644 --- a/wax-prosemirror-components/src/components/rightArea/BoxList.js +++ b/wax-prosemirror-components/src/components/rightArea/BoxList.js @@ -2,6 +2,7 @@ import { Mark } from 'prosemirror-model'; import React from 'react'; import ConnectedComment from '../comments/ConnectedComment'; +import ConnectedTrackChange from '../trackChanges/ConnectedTrackChange'; import TrackChangeBox from '../trackChanges/TrackChangeBox'; export default ({ commentsTracks, view, position, recalculateTops }) => { @@ -28,12 +29,13 @@ export default ({ commentsTracks, view, position, recalculateTops }) => { ); } return ( - <TrackChangeBox + <ConnectedTrackChange key={id} trackChange={commentTrack} view={view} top={top} dataBox={id} + recalculateTops={recalculateTops} /> ); })} diff --git a/wax-prosemirror-components/src/components/rightArea/RightArea.js b/wax-prosemirror-components/src/components/rightArea/RightArea.js index 961f93a3e3499d2b5469d053331d2dff958e62d2..ebbedc6ca85fe0002429427181af389e7b5e2150 100644 --- a/wax-prosemirror-components/src/components/rightArea/RightArea.js +++ b/wax-prosemirror-components/src/components/rightArea/RightArea.js @@ -66,11 +66,11 @@ export default ({ area }) => { if (markNodeEl) { boxEl = document.querySelector(`div[data-box="${id}"]`); } - if (boxEl) boxHeight = parseInt(boxEl.offsetHeight, 10); - - // where the box should move to - top = boxEl ? annotationTop : -2000; - + if (boxEl) { + boxHeight = parseInt(boxEl.offsetHeight, 10); + // where the box should move to + top = annotationTop; + } // if the above comment box has already taken up the height, move down if (pos > 0) { const previousBox = marksNodes[area][pos - 1]; diff --git a/wax-prosemirror-components/src/components/tables/CreateTable.js b/wax-prosemirror-components/src/components/tables/CreateTable.js index 31b2ec3821afb9b71e4b82f2ff0f9350016a3b39..42a08ffa972278bde01cb1b1fcf79b814d0d8c09 100644 --- a/wax-prosemirror-components/src/components/tables/CreateTable.js +++ b/wax-prosemirror-components/src/components/tables/CreateTable.js @@ -1,12 +1,13 @@ /* eslint react/prop-types: 0 */ -import React, { useState, useContext, useMemo, useEffect, useRef } from 'react'; +import React, { useState, useContext, useMemo, useRef } from 'react'; import { WaxContext } from 'wax-prosemirror-core'; import styled from 'styled-components'; import { grid } from '@pubsweet/ui-toolkit'; import MenuButton from '../../ui/buttons/MenuButton'; import InsertTableTool from '../../ui/tables/InsertTableTool'; +import useOnClickOutside from '../../helpers/useOnClickOutside'; const Wrapper = styled.div` font-size: 0; @@ -45,8 +46,7 @@ const CreateTable = ({ view = {}, item }) => { setIsOpen(!isOpen); }; - const isDisabled = - enable && !enable(state) && !(select && select(state, activeViewId)); + const isDisabled = !select(state, activeViewId); useOnClickOutside(ref, () => setIsOpen(false)); @@ -72,25 +72,4 @@ const CreateTable = ({ view = {}, item }) => { return MemorizedDropdown; }; -// Hook -const useOnClickOutside = (ref, handler) => { - useEffect(() => { - const listener = event => { - /* Do nothing if clicking ref's element or descendent elements */ - if (!ref.current || ref.current.contains(event.target)) { - return; - } - - handler(event); - }; - - document.addEventListener('mousedown', listener); - document.addEventListener('touchstart', listener); - - return () => { - document.removeEventListener('mousedown', listener); - document.removeEventListener('touchstart', listener); - }; - }, [ref, handler]); -}; export default CreateTable; diff --git a/wax-prosemirror-components/src/components/trackChanges/ConnectedTrackChange.js b/wax-prosemirror-components/src/components/trackChanges/ConnectedTrackChange.js new file mode 100644 index 0000000000000000000000000000000000000000..4d983e6572378901eeee40096b8a5048167fb8d5 --- /dev/null +++ b/wax-prosemirror-components/src/components/trackChanges/ConnectedTrackChange.js @@ -0,0 +1,56 @@ +/* eslint react/prop-types: 0 */ +import React, { useContext, useMemo } from 'react'; +import styled from 'styled-components'; +import { DocumentHelpers } from 'wax-prosemirror-utilities'; +import { WaxContext } from 'wax-prosemirror-core'; +import TrackChangesBox from '../../ui/trackChanges/TrackChangesBox'; + +const ConnectedTrackChangeStyled = styled.div` + position: absolute; + margin-left: ${props => (props.active ? `${-20}px` : `${50}px`)}; + width: 200px; + @media (max-width: 600px) { + margin-left: 15px; + } +`; + +export default ({ trackChangeId, top, recalculateTops }) => { + const { + view, + view: { + main: { + props: { user }, + }, + }, + app, + activeView, + } = useContext(WaxContext); + + const { state, dispatch } = activeView; + + const styles = { + top: `${top}px`, + }; + + const active = false; + return null; + const MemorizedTrackChange = useMemo( + () => ( + <ConnectedTrackChangeStyled + data-box={trackChangeId} + style={styles} + active={active} + > + <TrackChangesBox + key={trackChangeId} + active={active} + trackChangeId={trackChangeId} + commentData="" + recalculateTops={recalculateTops} + /> + </ConnectedTrackChangeStyled> + ), + [active, top], + ); + return <>{MemorizedTrackChange}</>; +}; diff --git a/wax-prosemirror-components/src/ui/comments/DateParser.js b/wax-prosemirror-components/src/helpers/DateParser.js similarity index 100% rename from wax-prosemirror-components/src/ui/comments/DateParser.js rename to wax-prosemirror-components/src/helpers/DateParser.js diff --git a/wax-prosemirror-components/src/ui/buttons/Icon.js b/wax-prosemirror-components/src/helpers/Icon.js similarity index 90% rename from wax-prosemirror-components/src/ui/buttons/Icon.js rename to wax-prosemirror-components/src/helpers/Icon.js index 00b46ac11e22f59e6dc3741ef0b3aee764d84ad5..0fedb3d648f9ccb844fad823492564841313ad4a 100644 --- a/wax-prosemirror-components/src/ui/buttons/Icon.js +++ b/wax-prosemirror-components/src/helpers/Icon.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import icons from '../../icons/icons'; +import icons from '../icons/icons'; /** * Only works with SVG icons from icons.js diff --git a/wax-prosemirror-components/src/helpers/useOnClickOutside.js b/wax-prosemirror-components/src/helpers/useOnClickOutside.js new file mode 100644 index 0000000000000000000000000000000000000000..25dffb0b9611c8214b26e0949efdf18f33336967 --- /dev/null +++ b/wax-prosemirror-components/src/helpers/useOnClickOutside.js @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; + +// Hook +const useOnClickOutside = (ref, handler) => { + useEffect(() => { + const listener = event => { + /* Do nothing if clicking ref's element or descendent elements */ + if (!ref.current || ref.current.contains(event.target)) { + return; + } + + handler(event); + }; + + document.addEventListener('mousedown', listener); + document.addEventListener('touchstart', listener); + + return () => { + document.removeEventListener('mousedown', listener); + document.removeEventListener('touchstart', listener); + }; + }, [ref, handler]); +}; + +export default useOnClickOutside; diff --git a/wax-prosemirror-components/src/icons/icons.js b/wax-prosemirror-components/src/icons/icons.js index 016facfe09198740296c882322f00dc13ead8f27..2e35a7fa4eb43c747ee3386fbc287dd4445c96cd 100644 --- a/wax-prosemirror-components/src/icons/icons.js +++ b/wax-prosemirror-components/src/icons/icons.js @@ -5,6 +5,8 @@ import React from 'react'; import styled from 'styled-components'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; +import { th } from '@pubsweet/ui-toolkit'; + import { faCheck, faParagraph, @@ -23,7 +25,7 @@ const Svg = styled.svg.attrs(() => ({ }))` height: 24px; width: 24px; - fill: gray; + fill: ${th('colorPrimary')}; vertical-align: top; `; @@ -102,6 +104,7 @@ export default { ), more: ({ className }) => ( <Svg className={className} viewBox="0 0 24 24"> + <title> Expand </title> <path d="M0 0h24v24H0z" fill="none" /> <path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" /> </Svg> @@ -218,4 +221,31 @@ export default { <path d="M5 4v3h5.5v12h3V7H19V4z" /> </Svg> ), + findAndReplace: ({ className }) => ( + <Svg className={className} viewBox="0 0 24 24" fill="none"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M11 6c1.38 0 2.63.56 3.54 1.46L12 10h6V4l-2.05 2.05C14.68 4.78 12.93 4 11 4c-3.53 0-6.43 2.61-6.92 6H6.1c.46-2.28 2.48-4 4.9-4zm5.64 9.14c.66-.9 1.12-1.97 1.28-3.14H15.9c-.46 2.28-2.48 4-4.9 4-1.38 0-2.63-.56-3.54-1.46L10 12H4v6l2.05-2.05C7.32 17.22 9.07 18 11 18c1.55 0 2.98-.51 4.14-1.36L20 21.49 21.49 20l-4.85-4.86z" /> + </Svg> + ), + navigatePrevious: ({ className }) => ( + <Svg className={className} viewBox="0 0 24 24" fill="none"> + <title>Previous</title> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" /> + </Svg> + ), + navigateNext: ({ className }) => ( + <Svg className={className} viewBox="0 0 24 24" fill="none"> + <title>Next</title> + <path d="M0 0h24v24H0V0z" fill="none" /> + <path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z" /> + </Svg> + ), + close: ({ className }) => ( + <Svg className={className} viewBox="0 0 24 24" fill="none"> + <title> Close </title> + <path d="M0 0h24v24H0V0z" fill="none" /> + <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" /> + </Svg> + ), }; diff --git a/wax-prosemirror-components/src/ui/buttons/MenuButton.js b/wax-prosemirror-components/src/ui/buttons/MenuButton.js index 6101c3d50fcd0b31e65db6cb7c77efaa63c497f0..92b255f15f6b88f02d90dd54ef877e88633fc0f2 100644 --- a/wax-prosemirror-components/src/ui/buttons/MenuButton.js +++ b/wax-prosemirror-components/src/ui/buttons/MenuButton.js @@ -4,7 +4,7 @@ import styled, { css } from 'styled-components'; import { th, override } from '@pubsweet/ui-toolkit'; -import Icon from './Icon'; +import Icon from '../../helpers/Icon'; const activeStyles = css` background: ${th('colorPrimary')}; diff --git a/wax-prosemirror-components/src/ui/comments/CommentBox.js b/wax-prosemirror-components/src/ui/comments/CommentBox.js index 806c8c0c50238b88dedaed8fde45341bedf24a50..591dfc2442826e5eda3a7cdeccf0f271312c9245 100644 --- a/wax-prosemirror-components/src/ui/comments/CommentBox.js +++ b/wax-prosemirror-components/src/ui/comments/CommentBox.js @@ -7,20 +7,20 @@ import CommentItemList from './CommentItemList'; import CommentReply from './CommentReply'; const inactive = css` - background: #e2e2e2; + background: ${th('colorBackgroundHue')}; cursor: pointer; transition: box-shadow 0.2s; /* transition: background-color 0.2s; */ &:hover { /* background: white; */ - box-shadow: 0 0 1px 2px gray; + box-shadow: 0 0 1px 2px ${th('colorBackgroundTabs')}; } `; const Wrapper = styled.div` background: white; - border: 1px solid gray; + border: 1px solid ${th('colorBackgroundTabs')}; border-radius: 3px; box-sizing: border-box; display: flex; @@ -38,18 +38,20 @@ const Head = styled.div` const Resolve = styled.button` align-self: flex-end; + border: none; + background: none; + color: #0042c7; cursor: pointer; margin-bottom: 12px; &:hover { - background: gold; - border-color: gold; - border-radius: 3px; + background: ${th('colorBackgroundHue')}; + border: none; } `; const StyledReply = styled(CommentReply)` - border-top: ${props => !props.isNewComment && '1px solid gray'}; + border-top: ${props => !props.isNewComment && `3px solid #E1EBFF`}; `; const CommentBox = props => { diff --git a/wax-prosemirror-components/src/ui/comments/CommentBubble.js b/wax-prosemirror-components/src/ui/comments/CommentBubble.js index 945138a1e815dfd815a65138503cc1e642a176e7..2ddae27ba424ff2570634ece488af1b5e666006f 100644 --- a/wax-prosemirror-components/src/ui/comments/CommentBubble.js +++ b/wax-prosemirror-components/src/ui/comments/CommentBubble.js @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; - +import { th } from '@pubsweet/ui-toolkit'; /** * SVG source * https://material.io/resources/icons/?search=chat&icon=chat&style=baseline @@ -40,7 +40,7 @@ const IconSVG = props => { const Icon = styled(IconSVG)` height: 28px; width: 28px; - fill: gray; + fill: ${th('colorPrimary')}; `; const Bubble = props => { diff --git a/wax-prosemirror-components/src/ui/comments/CommentItem.js b/wax-prosemirror-components/src/ui/comments/CommentItem.js index 0baeb8c2ebf37d5373ba13d32722709a3b9fda02..3afd06ba8351aa8cbed0d70c6d12c5181924e533 100644 --- a/wax-prosemirror-components/src/ui/comments/CommentItem.js +++ b/wax-prosemirror-components/src/ui/comments/CommentItem.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { grid, th } from '@pubsweet/ui-toolkit'; -import DateParser from './DateParser'; +import DateParser from '../../helpers/DateParser'; const Wrapper = styled.div``; diff --git a/wax-prosemirror-components/src/ui/comments/CommentReply.js b/wax-prosemirror-components/src/ui/comments/CommentReply.js index 5c84ec33adb23a66c34152f409ab526fec56289f..0eb05d4399ecc4d4722df8f1235c4b49dde13502 100644 --- a/wax-prosemirror-components/src/ui/comments/CommentReply.js +++ b/wax-prosemirror-components/src/ui/comments/CommentReply.js @@ -5,7 +5,7 @@ import styled, { css } from 'styled-components'; import { grid, th } from '@pubsweet/ui-toolkit'; const Wrapper = styled.div` - background: #e2e2e2; + background: ${th('colorBackgroundHue')}; display: flex; flex-direction: column; padding: ${grid(2)} ${grid(4)}; @@ -14,10 +14,16 @@ const Wrapper = styled.div` const TextWrapper = styled.div``; const ReplyTextArea = styled.textarea` - background: #e2e2e2; - box-sizing: border-box; + background: ${th('colorBackgroundHue')}; + border: 3px solid ${th('colorBackgroundTabs')}; + position: relative; + right: 5px; font-family: ${th('fontWriting')}; width: 100%; + + &:focus { + outline: 1px solid ${th('colorPrimary')}; + } `; const ActionWrapper = styled.div` @@ -27,7 +33,7 @@ const ActionWrapper = styled.div` `; const primary = css` - background: gray; + background: ${th('colorPrimary')}; color: white; `; @@ -73,7 +79,6 @@ const CommentReply = props => { const onBlur = content => { onTextAreaBlur(content, isNewComment); - setCommentValue(''); }; return ( @@ -85,6 +90,8 @@ const CommentReply = props => { onBlur={() => onBlur(commentInput.current.value)} placeholder={isNewComment ? 'Write comment...' : 'Reply...'} onChange={() => setCommentValue(commentInput.current.value)} + cols="5" + rows="3" onKeyDown={e => { if (e.keyCode === 13 && !e.shiftKey) { e.preventDefault(); diff --git a/wax-prosemirror-components/src/ui/inputs/CheckBox.js b/wax-prosemirror-components/src/ui/inputs/CheckBox.js new file mode 100644 index 0000000000000000000000000000000000000000..862b8600d6169c3f435dac16664f64ac3a81346b --- /dev/null +++ b/wax-prosemirror-components/src/ui/inputs/CheckBox.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled, { css } from 'styled-components'; + +import { th, override } from '@pubsweet/ui-toolkit'; + +const CheckBoxContainer = styled.div` + display: flex; + flex-direction: row; +`; +const CheckBoxLabel = styled.label` + display: block; + position: relative; + margin-right: 10px; + cursor: pointer; + font-size: 18px; + line-height: 20px; + height: 20px; + width: 20px; + clear: both; + + input { + position: absolute; + opacity: 0; + cursor: pointer; + } + + input:checked ~ span { + background-color: #ffffff; + border-radius: 5px; + -webkit-transform: rotate(0deg) scale(1); + -ms-transform: rotate(0deg) scale(1); + transform: rotate(0deg) scale(1); + opacity: 1; + border: 2px solid #4b5871; + } + + input:checked ~ span::after { + -webkit-transform: rotate(45deg) scale(1); + -ms-transform: rotate(45deg) scale(1); + transform: rotate(45deg) scale(1); + opacity: 1; + left: 6px; + top: 1px; + width: 6px; + height: 12px; + border: solid ${th('colorBackgroundButton')}; + border-width: 0 2px 2px 0; + background-color: transparent; + border-radius: 0; + } +`; +const CheckboxCustom = styled.span` + position: absolute; + top: 0px; + left: 0px; + height: 20px; + width: 20px; + background-color: transparent; + border-radius: 5px; + transition: all 0.3s ease-out; + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + border: 2px solid #4b5871; + + &:after { + position: absolute; + content: ''; + left: 12px; + top: 12px; + height: 0px; + width: 0px; + border-radius: 5px; + border: solid ${th('colorBackgroundButton')}; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(0deg) scale(0); + -ms-transform: rotate(0deg) scale(0); + transform: rotate(0deg) scale(0); + opacity: 1; + transition: all 0.3s ease-out; + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + } +`; +const CheckboxTitle = styled.div` + color: #4b5871; + position: relative; + top: 2px; + font-size: 15px; + margin-left: 30px; + width: 200px; +`; + +const CheckBox = props => { + const { name, label } = props; + return ( + <CheckBoxContainer> + <CheckBoxLabel> + <input type="checkbox" id={name} name={name} /> + <CheckboxCustom /> + <CheckboxTitle>{label}</CheckboxTitle> + </CheckBoxLabel> + </CheckBoxContainer> + ); +}; + +CheckBox.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, +}; + +CheckBox.defaultProps = { + label: null, +}; + +export default CheckBox; diff --git a/wax-prosemirror-components/src/ui/tabs/BlockElement.js b/wax-prosemirror-components/src/ui/tabs/BlockElement.js index 4dd07887cebaddf5d2a6ffb2e3e61d94929729c9..64c7e5ab4ff2f0be30ca51be3d5e536470899d61 100644 --- a/wax-prosemirror-components/src/ui/tabs/BlockElement.js +++ b/wax-prosemirror-components/src/ui/tabs/BlockElement.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { th } from '@pubsweet/ui-toolkit'; import Button from '../../components/Button'; @@ -10,14 +9,21 @@ const Wrapper = styled.div` button { border-radius: 4px; margin-left: 4px; + padding-left: 25px; + position: relative; + left: -33px; } `; const Box = styled.div` - width: 28px; - height: 28px; + width: 22px; + height: 22px; + position: relative; + top: 3px; + right: 3px; border-radius: 4px; - background: gray; + background: #bfc4cd; + z-index: 999; `; const StyledButton = styled(Button)``; diff --git a/wax-prosemirror-components/src/ui/tabs/Tabs.js b/wax-prosemirror-components/src/ui/tabs/Tabs.js index f460938b6d2ce7c09da8ffca26129a0cc1a8e9c4..06519ce399573f7acf0b2f633b9419f04681b7bf 100644 --- a/wax-prosemirror-components/src/ui/tabs/Tabs.js +++ b/wax-prosemirror-components/src/ui/tabs/Tabs.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { th } from '@pubsweet/ui-toolkit'; import PropTypes from 'prop-types'; import styled, { css } from 'styled-components'; -import Icon from '../buttons/Icon'; +import Icon from '../../helpers/Icon'; const Wrapper = styled.div` display: flex; @@ -12,15 +12,15 @@ const Wrapper = styled.div` const Tabs = styled.div` display: flex; flex-direction: column; - background: #e3e3e3; + background: #fff; `; const activeTab = css` - box-shadow: 0 0 3px ${th('colorPrimary')}; + box-shadow: 0 0 1px ${th('colorPrimary')}; + background: ${th('colorBackgroundTabs')}; `; const Tab = styled.div` - background: gainsboro; padding: 8px; margin: 0 4px 4px 4px; cursor: pointer; @@ -32,14 +32,14 @@ const Tab = styled.div` ${props => props.active && activeTab} &:hover { - background: silver; + background: ${th('colorBackgroundTabs')}; } `; const Content = styled.div` width: 100%; height: 100%; - background: gainsboro; + background: #fff; `; const TabsPane = props => { diff --git a/wax-prosemirror-core/package.json b/wax-prosemirror-core/package.json index 1bacd15961ecf39af7a5950f1ef87bc85772607b..cc4d3fd093a5a3678ea2b97948962af394f8b3b0 100644 --- a/wax-prosemirror-core/package.json +++ b/wax-prosemirror-core/package.json @@ -4,7 +4,7 @@ "version": "0.0.20", "description": "Wax prosemirror core", "license": "MIT", - "main": "index.js", + "main": "dist/index.js", "files": [ "dist" ], diff --git a/wax-prosemirror-core/src/WaxContext.js b/wax-prosemirror-core/src/WaxContext.js index b5a934509a6907f93bfc39f2795fd1002a4e588c..6889bc6a0ddb607d883aba7966ae85121b4262b1 100644 --- a/wax-prosemirror-core/src/WaxContext.js +++ b/wax-prosemirror-core/src/WaxContext.js @@ -18,10 +18,8 @@ export default props => { view: props.view || {}, activeView: props.activeView || {}, activeViewId: props.activeViewId || {}, - removeView: view => { - const newContext = { ...context, view }; - - setContext({ ...newContext }); + removeView: deletedView => { + delete context.view[deletedView]; }, updateView: (newView, activeViewId) => { const view = Object.assign(context.view, newView); diff --git a/wax-prosemirror-core/src/WaxView.js b/wax-prosemirror-core/src/WaxView.js index d8722a6a8465608a1b918398f3f31f71c611e95d..43147ae4e154e5329697da9b3cc76fe9b17ca413 100644 --- a/wax-prosemirror-core/src/WaxView.js +++ b/wax-prosemirror-core/src/WaxView.js @@ -58,7 +58,7 @@ export default props => { if (autoFocus) setTimeout(() => { view.focus(); - }); + }, 1000); return () => view.destroy(); } diff --git a/wax-prosemirror-plugins/index.js b/wax-prosemirror-plugins/index.js index 819d258b5e5fcd2f7faa176493b63799d35f2056..b6732428ae192dfafffe0b2638d493264486f89a 100644 --- a/wax-prosemirror-plugins/index.js +++ b/wax-prosemirror-plugins/index.js @@ -6,3 +6,4 @@ export { default as highlightPlugin } from './src/highlightPlugin'; export { default as mathPlugin } from './src/math/math-plugin'; export { default as mathSelectPlugin } from './src/math/math-select'; +export { default as FindAndReplacePlugin } from './src/findAndReplace/FindAndReplacePlugin'; diff --git a/wax-prosemirror-plugins/package.json b/wax-prosemirror-plugins/package.json index 86635e611941ae990749878a9fb698a1895eec38..db063b2f55870e939d2cbcd8ab5bab0563111e19 100644 --- a/wax-prosemirror-plugins/package.json +++ b/wax-prosemirror-plugins/package.json @@ -4,7 +4,7 @@ "version": "0.0.20", "description": "Wax prosemirror plugins", "license": "MIT", - "main": "index.js", + "main": "dist/index.js", "files": [ "dist" ], diff --git a/wax-prosemirror-plugins/src/FindAndReplacePlugin.js b/wax-prosemirror-plugins/src/FindAndReplacePlugin.js deleted file mode 100644 index 96b65597222bb19213a5633fb5f8a1b061713bf2..0000000000000000000000000000000000000000 --- a/wax-prosemirror-plugins/src/FindAndReplacePlugin.js +++ /dev/null @@ -1,107 +0,0 @@ -import ReactDOM from "react-dom"; -import React from "react"; -import { EditorState, Plugin, PluginKey } from "prosemirror-state"; -import { TextSelection } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; - -const Component = ({ state }) => { - return <div>11111{state.selection.from}</div>; -}; - -const findNodesWithSameMark = (doc, from, to, markType) => { - let ii = from; - const finder = mark => mark.type === markType; - let firstMark = null; - let fromNode = null; - let toNode = null; - - while (ii <= to) { - const node = doc.nodeAt(ii); - if (!node || !node.marks) { - return null; - } - const mark = node.marks.find(finder); - if (!mark) { - return null; - } - if (firstMark && mark !== firstMark) { - return null; - } - fromNode = fromNode || node; - firstMark = firstMark || mark; - toNode = node; - ii++; - } - - let fromPos = from; - let toPos = to; - - let jj = 0; - ii = from - 1; - while (ii > jj) { - const node = doc.nodeAt(ii); - const mark = node && node.marks.find(finder); - if (!mark || mark !== firstMark) { - break; - } - fromPos = ii; - fromNode = node; - ii--; - } - - ii = to + 1; - jj = doc.nodeSize - 2; - while (ii < jj) { - const node = doc.nodeAt(ii); - const mark = node && node.marks.find(finder); - if (!mark || mark !== firstMark) { - break; - } - toPos = ii; - toNode = node; - ii++; - } - - return { - mark: firstMark, - from: { - node: fromNode, - pos: fromPos - }, - to: { - node: toNode, - pos: toPos - } - }; -}; - -const WithStatePlugin = Component => ({ state }) => { - // const { doc, selection, schema } = state; - // const markType = schema.marks.strong; - // if (!markType) { - // return null; - // } - // const { from, to } = selection; - // const result = findNodesWithSameMark(doc, from, to, markType); - //return result ? <Component state={state} /> : null; - return <Component state={state} />; -}; - -export const FindAndReplaceKey = new PluginKey("findandreplace"); - -const FindAndReplacePlugin = new Plugin({ - key: FindAndReplaceKey, - state: { - init() { - return { - renderArea: "rightSideBar", - component: WithStatePlugin(Component) - }; - }, - apply(tr, oldState, newState) { - return this.getState(newState); - } - } -}); - -export default FindAndReplacePlugin; diff --git a/wax-prosemirror-plugins/src/comments/CommentPlugin.js b/wax-prosemirror-plugins/src/comments/CommentPlugin.js index 2d61ca2aa537faecd218b37418e50f37684c5250..5b292dd7648d34722c2c152790c19c53a9e54243 100644 --- a/wax-prosemirror-plugins/src/comments/CommentPlugin.js +++ b/wax-prosemirror-plugins/src/comments/CommentPlugin.js @@ -93,7 +93,6 @@ export default props => { const commentPluginState = state && commentPlugin.getState(state); return commentPluginState.createDecoration; }, - setCommentActive: state => {}, }, }); }; diff --git a/wax-prosemirror-plugins/src/findAndReplace/FindAndReplacePlugin.js b/wax-prosemirror-plugins/src/findAndReplace/FindAndReplacePlugin.js new file mode 100644 index 0000000000000000000000000000000000000000..d625d7b5a33de22718c00901852e5cb8e4474dc2 --- /dev/null +++ b/wax-prosemirror-plugins/src/findAndReplace/FindAndReplacePlugin.js @@ -0,0 +1,102 @@ +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; +import { eachRight } from 'lodash'; + +const findAndReplacePlugin = new PluginKey('findAndReplacePlugin'); + +let searchText = ''; + +const findMatches = (doc, searchValue) => { + const allNodes = []; + + doc.descendants((node, pos) => { + allNodes.push({ node, pos }); + }); + + eachRight(allNodes, (node, index) => { + if (node.node.type.name === 'footnote') { + allNodes.splice(index + 1, node.node.childCount); + } + }); + + const results = []; + const mergedTextNodes = []; + let index = 0; + + allNodes.forEach((node, i) => { + if (node.node.isText) { + if (mergedTextNodes[index]) { + mergedTextNodes[index] = { + text: mergedTextNodes[index].text + node.node.text, + pos: mergedTextNodes[index].pos, + }; + } else { + mergedTextNodes[index] = { + text: node.node.text, + pos: node.pos, + }; + } + } else { + index += 1; + } + }); + mergedTextNodes.forEach(({ text, pos }) => { + const search = RegExp(searchValue, 'gui'); + let m; + // eslint-disable-next-line no-cond-assign + while ((m = search.exec(text))) { + if (m[0] === '') { + break; + } + + results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }); + } + }); + return results; +}; + +export default props => { + return new Plugin({ + key: findAndReplacePlugin, + state: { + init: (_, state) => { + return DecorationSet.empty; + }, + apply(tr, prev, _, newState) { + let decorations; + let createdDecorations = DecorationSet.empty; + const allMatches = findMatches(newState.doc, searchText); + if (allMatches.length > 0) { + decorations = allMatches.map((result, index) => { + return Decoration.inline(result.from, result.to, { + class: 'search-result', + }); + }); + createdDecorations = DecorationSet.create(newState.doc, decorations); + } + return { + createdDecorations, + allMatches, + }; + }, + }, + props: { + decorations: state => { + const findAndReplacePluginState = + state && findAndReplacePlugin.getState(state); + return findAndReplacePluginState.createdDecorations; + }, + setSearchText: text => { + searchText = text; + }, + }, + view(editorState) { + return { + update: (view, previousState) => {}, + }; + }, + }); +}; diff --git a/wax-prosemirror-schema/package.json b/wax-prosemirror-schema/package.json index f8f677018f65ed4a3df98793f7dfe66907a7ff93..0fc629183eead3283069d1f32a872eb43382d97c 100644 --- a/wax-prosemirror-schema/package.json +++ b/wax-prosemirror-schema/package.json @@ -4,7 +4,7 @@ "version": "0.0.20", "description": "Wax prosemirror schema", "license": "MIT", - "main": "index.js", + "main": "dist/index.js", "files": [ "dist" ], diff --git a/wax-prosemirror-services/index.js b/wax-prosemirror-services/index.js index 6171f6a4a996f36be204f37cfd4b2fec809645c7..59998e9685ec1d5549d3f091b09741f61c3c8fab 100644 --- a/wax-prosemirror-services/index.js +++ b/wax-prosemirror-services/index.js @@ -28,6 +28,7 @@ export { default as LinkService } from './src/LinkService/LinkService'; export { default as TrackChangeService } from './src/TrackChangeService/TrackChangeService'; export { default as trackedTransaction } from './src/TrackChangeService/track-changes/trackedTransaction'; export { default as MathService } from './src/MathService/MathService'; +export { default as FindAndReplaceService } from './src/FindAndReplaceService/FindAndReplaceService'; /* ToolGroups @@ -42,6 +43,6 @@ export { default as TextToolGroupService } from './src/WaxToolGroups/TextToolGro export { default as NoteToolGroupService } from './src/WaxToolGroups/NoteToolGroupService/NoteToolGroupService'; export { default as CodeBlockToolGroupService } from './src/WaxToolGroups/CodeBlockToolGroupService/CodeBlockToolGroupService'; export { default as TrackChangeToolGroupService } from './src/WaxToolGroups/TrackChangeToolGroupService/TrackChangeToolGroupService'; - export { default as DisplayTextToolGroupService } from './src/WaxToolGroups/DisplayTextToolGroupService/DisplayTextToolGroupService'; export { default as BlockDropDownToolGroupService } from './src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDownToolGroupService'; +export { default as TrackingAndEditingToolGroupService } from './src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditingToolGroupService'; diff --git a/wax-prosemirror-services/package.json b/wax-prosemirror-services/package.json index 60282baf22c8b50cfbb1aa55994b2693a14d4b68..14b33a13d28322d14365ff4991695b26deaa6015 100644 --- a/wax-prosemirror-services/package.json +++ b/wax-prosemirror-services/package.json @@ -4,7 +4,7 @@ "version": "0.0.20", "description": "Wax prosemirror services", "license": "MIT", - "main": "index.js", + "main": "dist/index.js", "files": [ "dist" ], @@ -27,6 +27,7 @@ "prosemirror-view": "1.15.2", "styled-components": "^4.2.0", "uuid": "^7.0.3", + "use-deep-compare-effect": "^1.3.1", "wax-prosemirror-components": "^0.0.20", "wax-prosemirror-core": "^0.0.20", "wax-prosemirror-plugins": "^0.0.20", diff --git a/wax-prosemirror-services/src/FindAndReplaceService/FindAndReplace.js b/wax-prosemirror-services/src/FindAndReplaceService/FindAndReplace.js new file mode 100644 index 0000000000000000000000000000000000000000..87472e990d410b335e3639fbe07ef34c4bb9845b --- /dev/null +++ b/wax-prosemirror-services/src/FindAndReplaceService/FindAndReplace.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { isEmpty } from 'lodash'; +import { v4 as uuidv4 } from 'uuid'; +import { injectable } from 'inversify'; +import { FindAndReplaceTool } from 'wax-prosemirror-components'; +import Tools from '../lib/Tools'; + +export default +@injectable() +class FindAndReplace extends Tools { + title = 'Find And Replace'; + icon = 'findAndReplace'; + name = 'find'; + + get run() { + return (state, dispatch) => {}; + } + + select = (state, activeViewId) => {}; + + get enable() { + return state => { + return true; + }; + } + + renderTool(view) { + if (isEmpty(view)) return null; + + return this._isDisplayed ? ( + <FindAndReplaceTool key={uuidv4()} item={this.toJSON()} view={view} /> + ) : null; + } +} diff --git a/wax-prosemirror-services/src/FindAndReplaceService/FindAndReplaceService.js b/wax-prosemirror-services/src/FindAndReplaceService/FindAndReplaceService.js new file mode 100644 index 0000000000000000000000000000000000000000..fbe424b88eb23b33b21084b8c8d32c71a817608e --- /dev/null +++ b/wax-prosemirror-services/src/FindAndReplaceService/FindAndReplaceService.js @@ -0,0 +1,19 @@ +import { FindAndReplacePlugin } from 'wax-prosemirror-plugins'; +import Service from '../Service'; +import FindAndReplace from './FindAndReplace'; + +class FindAndReplaceService extends Service { + name = 'FindAndReplaceService'; + + boot() { + this.app.PmPlugins.add( + 'findAndReplacePlugin', + FindAndReplacePlugin('findAndReplacePlugin'), + ); + } + + register() { + this.container.bind('FindAndReplace').to(FindAndReplace); + } +} +export default FindAndReplaceService; diff --git a/wax-prosemirror-services/src/ImageService/ImageService.js b/wax-prosemirror-services/src/ImageService/ImageService.js index 526d22e5259691429a1c703e9eb08cef3e93b438..7201e208ed15cf595d275d659d3b1c114b5bc947 100644 --- a/wax-prosemirror-services/src/ImageService/ImageService.js +++ b/wax-prosemirror-services/src/ImageService/ImageService.js @@ -2,7 +2,7 @@ import { imageNode } from 'wax-prosemirror-schema'; import Service from '../Service'; import Image from './Image'; -export default class ImageService extends Service { +class ImageService extends Service { name = 'ImageService'; register() { @@ -16,3 +16,5 @@ export default class ImageService extends Service { ); } } + +export default ImageService; diff --git a/wax-prosemirror-services/src/NoteService/Editor.js b/wax-prosemirror-services/src/NoteService/Editor.js index cc569e9c8299fe0f5d3a458278600239979ce53a..b8bcb437df04ef465e2cce279185fb02413dcf88 100644 --- a/wax-prosemirror-services/src/NoteService/Editor.js +++ b/wax-prosemirror-services/src/NoteService/Editor.js @@ -43,9 +43,7 @@ export default ({ node, view }) => { transformPasted: slice => { return transformPasted(slice, noteView); }, - handleKeyPress: (noteEditorView, from, to, content) => { - updateMainView = false; - }, + attributes: { spellcheck: 'false', }, @@ -88,11 +86,9 @@ export default ({ node, view }) => { }); // TODO Remove timeout and use state to check if noteView has changed - if (updateMainView) { - setTimeout(() => { - context.updateView({}, noteId); - }, 20); - } + setTimeout(() => { + context.updateView({}, noteId); + }, 20); if (!tr.getMeta('fromOutside')) { const outerTr = view.state.tr; @@ -105,7 +101,6 @@ export default ({ node, view }) => { if (outerTr.docChanged) view.dispatch(outerTr.setMeta('outsideView', 'notes')); - updateMainView = true; } }; diff --git a/wax-prosemirror-services/src/NoteService/NoteComponent.js b/wax-prosemirror-services/src/NoteService/NoteComponent.js index a24fd02d393b897195981f2813a0c52085624c6e..b48136662c26f2b5ca1eb24f7f64197e44bc8013 100644 --- a/wax-prosemirror-services/src/NoteService/NoteComponent.js +++ b/wax-prosemirror-services/src/NoteService/NoteComponent.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React, { useContext, useState, useMemo } from 'react'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { differenceBy } from 'lodash'; @@ -5,38 +6,48 @@ import { WaxContext } from 'wax-prosemirror-core'; import { DocumentHelpers } from 'wax-prosemirror-utilities'; import NoteEditor from './NoteEditor'; -export default () => { +export default ({ view: view }) => { + if (typeof view === 'undefined') return null; + const context = useContext(WaxContext); + const { - view, - view: { main }, - } = useContext(WaxContext); + state: { tr }, + } = view; const [notes, setNotes] = useState([]); - const cleanUpNoteViews = () => { if (view) { const currentNotes = DocumentHelpers.findChildrenByType( - main.state.doc, - main.state.schema.nodes.footnote, + view.state.doc, + view.state.schema.nodes.footnote, true, ); if (notes.length > currentNotes.length) { - // TODO remove from context views that no loger exist const difference = differenceBy(notes, currentNotes, 'node.attrs.id'); difference.forEach((item, i) => { - // delete view[item.node.attrs.id]; + context.removeView(item.node.attrs.id); }); + + tr.setMeta('notesChanged', true); + view.dispatch(tr); + + // const newView = Object.keys(view).reduce((object, key) => { + // if (key !== difference[0].node.attrs.id) { + // object[key] = view[key]; + // } + // return object; + // }, {}); } } }; useDeepCompareEffect(() => { - setNotes(updateNotes(main)); - // cleanUpNoteViews(); - }, [updateNotes(main)]); + setNotes(updateNotes(view)); + cleanUpNoteViews(); + }, [updateNotes(view)]); const noteComponent = useMemo( - () => <NoteEditor notes={notes} view={main} />, + () => <NoteEditor notes={notes} view={view} />, [notes], ); return <>{noteComponent}</>; diff --git a/wax-prosemirror-services/src/SchemaService/DefaultSchema.js b/wax-prosemirror-services/src/SchemaService/DefaultSchema.js index 23653b144e7c4f1ba734999afe5ddecba5bb721f..26295da1435fc629db431c5f0e750133663e1c70 100644 --- a/wax-prosemirror-services/src/SchemaService/DefaultSchema.js +++ b/wax-prosemirror-services/src/SchemaService/DefaultSchema.js @@ -1,49 +1,49 @@ -import { SchemaHelpers } from "wax-prosemirror-utilities"; +import { SchemaHelpers } from 'wax-prosemirror-utilities'; export default { nodes: { doc: { - content: "block+" + content: 'block+', }, text: { - group: "inline" + group: 'inline', }, hard_break: { inline: true, - group: "inline", + group: 'inline', selectable: false, - parseDOM: [{ tag: "br" }], + parseDOM: [{ tag: 'br' }], toDOM() { - return ["br"]; - } + return ['br']; + }, }, paragraph: { - group: "block", - content: "inline*", + group: 'block', + content: 'inline*', attrs: { - id: { default: "" }, - class: { default: "paragraph" }, + id: { default: '' }, + class: { default: 'paragraph' }, track: { default: [] }, - group: { default: "" } + group: { default: '' }, }, parseDOM: [ { - tag: "p.paragraph", + tag: 'p.paragraph', getAttrs(dom) { return { id: dom.dataset.id, - class: dom.getAttribute("class"), + class: dom.getAttribute('class'), track: SchemaHelpers.parseTracks(dom.dataset.track), - group: dom.dataset.group + group: dom.dataset.group, }; - } - } + }, + }, ], toDOM(node) { const attrs = SchemaHelpers.blockLevelToDOM(node); - return ["p", attrs, 0]; - } - } + return ['p', attrs, 0]; + }, + }, }, - marks: {} + marks: {}, }; diff --git a/wax-prosemirror-services/src/ShortCutsService/ShortCuts.js b/wax-prosemirror-services/src/ShortCutsService/ShortCuts.js index 3d5ceac0c6495a988d231ec74fd5b26d1d2bc8b2..87660df9becf605ccfc296c75e60cda40c369451 100644 --- a/wax-prosemirror-services/src/ShortCutsService/ShortCuts.js +++ b/wax-prosemirror-services/src/ShortCutsService/ShortCuts.js @@ -51,7 +51,7 @@ const backSpaceShortCut = (state, dispatch, view) => { } }); - backSpace( + return backSpace( state, tr => dispatch(tr.setMeta('inputType', 'backwardsDelete')), view, diff --git a/wax-prosemirror-services/src/TrackChangeService/AcceptTrackChangeService/AcceptTrackChange.js b/wax-prosemirror-services/src/TrackChangeService/AcceptTrackChangeService/AcceptTrackChange.js index d4debc43ae838a8e90c3ae21a6e32f7ebb3efb82..1d1c1ba1c60ed5aeefb30054d8f0c8c2cf2da2e1 100644 --- a/wax-prosemirror-services/src/TrackChangeService/AcceptTrackChangeService/AcceptTrackChange.js +++ b/wax-prosemirror-services/src/TrackChangeService/AcceptTrackChangeService/AcceptTrackChange.js @@ -104,10 +104,10 @@ class AcceptTrackChange extends Tools { }; } - select = (state, activeViewId) => { + select = (state, activeViewId, activeView) => { const { selection: { from, to }, - } = state; + } = activeView.state; if (from === to && activeViewId !== 'main') return false; return true; }; diff --git a/wax-prosemirror-services/src/TrackChangeService/RejectTrackChangeService/RejectTrackChange.js b/wax-prosemirror-services/src/TrackChangeService/RejectTrackChangeService/RejectTrackChange.js index 4a7e4de3dc9928364c38d88208d38b9a65369817..bd9dfcd83761ed0b98cf0359479c828a7b0e57f7 100644 --- a/wax-prosemirror-services/src/TrackChangeService/RejectTrackChangeService/RejectTrackChange.js +++ b/wax-prosemirror-services/src/TrackChangeService/RejectTrackChangeService/RejectTrackChange.js @@ -112,10 +112,10 @@ class RejectTrackChange extends Tools { }; } - select = (state, activeViewId) => { + select = (state, activeViewId, activeView) => { const { selection: { from, to }, - } = state; + } = activeView.state; if (from === to && activeViewId !== 'main') return false; return true; }; diff --git a/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js b/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js index b8a6f071aa8adc12d2ea32ed8c2adca87fb91d30..b937de7c3c64cf5d6e614e0e03f24552ee477255 100644 --- a/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js +++ b/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js @@ -30,7 +30,11 @@ const trackedTransaction = (tr, state, user, group = 'main') => { let node; if ($pos.depth === 1) { node = $pos.node($pos.depth); - if (node.content.content[0].type.name === 'image') return tr; + if ( + node.content.content[0] && + node.content.content[0].type.name === 'image' + ) + return tr; } } diff --git a/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDown.js b/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDown.js index 9a00598356383f9c6c0e91fd24c7232bb5179394..986894b5b6bd52bca8c4774f7d175eda31fd1af5 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDown.js +++ b/wax-prosemirror-services/src/WaxToolGroups/BlockDropDownToolGroupService/BlockDropDown.js @@ -48,7 +48,7 @@ class BlockDropDown extends ToolGroup { ]; } renderTools(view) { - if (isEmpty(view)) return null; + if (isEmpty(view) || window.innerWidth > 600) return null; const { activeViewId } = useContext(WaxContext); diff --git a/wax-prosemirror-services/src/WaxToolGroups/DisplayToolGroupService/Display.js b/wax-prosemirror-services/src/WaxToolGroups/DisplayToolGroupService/Display.js index 60b888079cbf5c6a995142cde9d996a929f65a97..e13f376e21abab21675848bb0da8ad14af66cedc 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/DisplayToolGroupService/Display.js +++ b/wax-prosemirror-services/src/WaxToolGroups/DisplayToolGroupService/Display.js @@ -1,22 +1,22 @@ -import React from "react"; -import { injectable, inject } from "inversify"; -import { LeftMenuTitle } from "wax-prosemirror-components"; -import ToolGroup from "../../lib/ToolGroup"; +import React from 'react'; +import { injectable, inject } from 'inversify'; +import { LeftMenuTitle } from 'wax-prosemirror-components'; +import ToolGroup from '../../lib/ToolGroup'; @injectable() class Display extends ToolGroup { tools = []; - title = <LeftMenuTitle title="Display" />; + title = (<LeftMenuTitle title="Display" />); constructor( - @inject("Author") author, - @inject("Title") title, - @inject("SubTitle") subtitle, - @inject("EpigraphProse") epigraphprose, - @inject("EpigraphPoetry") epigraphpoetry, - @inject("Heading1") heading1, - @inject("Heading2") heading2, - @inject("Heading3") heading3 + @inject('Author') author, + @inject('Title') title, + @inject('SubTitle') subtitle, + @inject('EpigraphProse') epigraphprose, + @inject('EpigraphPoetry') epigraphpoetry, + @inject('Heading1') heading1, + @inject('Heading2') heading2, + @inject('Heading3') heading3, ) { super(); this.tools = [ @@ -27,7 +27,7 @@ class Display extends ToolGroup { epigraphpoetry, heading1, heading2, - heading3 + heading3, ]; } } diff --git a/wax-prosemirror-services/src/WaxToolGroups/TextToolGroupService/Text.js b/wax-prosemirror-services/src/WaxToolGroups/TextToolGroupService/Text.js index c09c5a0f000ca50f0f4d4a32806fbd00a53e392b..8c9629cf5c8269034553ebd2382c5855d4f1afad 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/TextToolGroupService/Text.js +++ b/wax-prosemirror-services/src/WaxToolGroups/TextToolGroupService/Text.js @@ -1,20 +1,20 @@ -import React from "react"; -import { injectable, inject } from "inversify"; -import { LeftMenuTitle } from "wax-prosemirror-components"; -import ToolGroup from "../../lib/ToolGroup"; +import React from 'react'; +import { injectable, inject } from 'inversify'; +import { LeftMenuTitle } from 'wax-prosemirror-components'; +import ToolGroup from '../../lib/ToolGroup'; @injectable() class Text extends ToolGroup { tools = []; - title = <LeftMenuTitle title="Text" />; + title = (<LeftMenuTitle title="Text" />); constructor( - @inject("Paragraph") paragraph, - @inject("ParagraphContinued") paragraphContinued, - @inject("ExtractProse") extractProse, - @inject("ExtractPoetry") extractPoetry, - @inject("SourceNote") sourceNote, - @inject("BlockQuote") blockQuote + @inject('Paragraph') paragraph, + @inject('ParagraphContinued') paragraphContinued, + @inject('ExtractProse') extractProse, + @inject('ExtractPoetry') extractPoetry, + @inject('SourceNote') sourceNote, + @inject('BlockQuote') blockQuote, ) { super(); this.tools = [ @@ -23,7 +23,7 @@ class Text extends ToolGroup { extractProse, extractPoetry, sourceNote, - blockQuote + blockQuote, ]; } } diff --git a/wax-prosemirror-services/src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditing.js b/wax-prosemirror-services/src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditing.js new file mode 100644 index 0000000000000000000000000000000000000000..3c7b6bd87936319c3a8888ad358408fc289dbb71 --- /dev/null +++ b/wax-prosemirror-services/src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditing.js @@ -0,0 +1,23 @@ +import { injectable, inject } from 'inversify'; +import ToolGroup from '../../lib/ToolGroup'; + +@injectable() +class TrackingAndEditing extends ToolGroup { + tools = []; + constructor( + @inject('EnableTrackChange') enableTrackChange, + @inject('AcceptTrackChange') acceptTrackChange, + @inject('RejectTrackChange') rejectTrackChange, + @inject('FindAndReplace') findAndReplace, + ) { + super(); + this.tools = [ + findAndReplace, + enableTrackChange, + acceptTrackChange, + rejectTrackChange, + ]; + } +} + +export default TrackingAndEditing; diff --git a/wax-prosemirror-services/src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditingToolGroupService.js b/wax-prosemirror-services/src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditingToolGroupService.js new file mode 100644 index 0000000000000000000000000000000000000000..3d206b1339d88ac23db8de0f133a47cbfd795290 --- /dev/null +++ b/wax-prosemirror-services/src/WaxToolGroups/TrackingAndEditingToolGroupService/TrackingAndEditingToolGroupService.js @@ -0,0 +1,10 @@ +import Service from '../../Service'; +import TrackingAndEditing from './TrackingAndEditing'; + +class TrackingAndEditingToolGroupService extends Service { + register() { + this.container.bind('TrackingAndEditing').to(TrackingAndEditing); + } +} + +export default TrackingAndEditingToolGroupService; diff --git a/wax-prosemirror-services/src/lib/ToolGroup.js b/wax-prosemirror-services/src/lib/ToolGroup.js index efe1c529b940c81b97e3a2768e27aa53302ea0e2..3ab8f96c57c804aef8b4131a1b2ba2935bf964bf 100644 --- a/wax-prosemirror-services/src/lib/ToolGroup.js +++ b/wax-prosemirror-services/src/lib/ToolGroup.js @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { injectable } from 'inversify'; import { ToolGroupComponent, ToolGroups } from 'wax-prosemirror-components'; import { v4 as uuidv4 } from 'uuid'; +import { isEmpty } from 'lodash'; export default @injectable() @@ -57,6 +58,8 @@ class ToolGroup { } renderTools(view) { + if (isEmpty(view)) return null; + const { name } = this.constructor; if (this._toolGroups > 0) { return <ToolGroups toolGroups={this._toolGroups} view={view} />; @@ -72,7 +75,7 @@ class ToolGroup { name={name} /> ), - [view], + [], ); return MemorizedToolGroupComponent; diff --git a/yarn.lock b/yarn.lock index 85d6337242045a0ffa7e0089193c40e426b7c43e..9e556f69f0b79571ac45c3a0beb620fafaf3ec25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17493,6 +17493,11 @@ typeface-fira-sans-condensed@^0.0.54: resolved "https://registry.yarnpkg.com/typeface-fira-sans-condensed/-/typeface-fira-sans-condensed-0.0.54.tgz#5fe692b4d1ec4083f198e22b8bb1e8b1ed541db0" integrity sha512-Y/+UOdN75s1hpOoJhSavN1eTfmXLrUPHDeo2syCpnq18PAKQpglvRRM+3pDWtCjsFbni8N/1brgEV94Z9FNI9g== +typeface-inter@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/typeface-inter/-/typeface-inter-1.1.13.tgz#e6ce2acbfed8ae97f4c3f4b205b5cb26c9e24628" + integrity sha512-XHTUTR+qqsVAtr1mjJBu3f5q4e3q6PhuhLXJhQbRGKwqLrQmhC7dCoZ5n5Vd+dEmZvc3TdHhOEYfjI01xzM7HA== + typeface-vollkorn@^0.0.54: version "0.0.54" resolved "https://registry.yarnpkg.com/typeface-vollkorn/-/typeface-vollkorn-0.0.54.tgz#1288bcd7d81c3dd7cd419e4448580d2a0b0640b2"