diff --git a/package.json b/package.json index b9c0d58fa..c6befebfe 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,6 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.7.0", "prettier": "^1.19.1", - "react": "^16.8.6", - "react-dom": "^16.8.6", "source-map-loader": "^0.2.4", "ts-loader": "^6.2.1", "webpack": "^4.29.6", diff --git a/src/components/TagMenu.tsx b/src/components/TagMenu.tsx new file mode 100644 index 000000000..24eaf7f4e --- /dev/null +++ b/src/components/TagMenu.tsx @@ -0,0 +1,228 @@ +import * as React from 'react'; +import { findChildren } from 'prosemirror-utils'; +import { EditorView } from "prosemirror-view"; +import { v4 as uuidv4 } from 'uuid'; +import styled from "styled-components"; +import { CloseIcon } from "outline-icons"; + +type Props = { + view: EditorView; +}; + +type TagListProps = { + key: string; + selectedKey: string; + tagName: string; + deleteTag: () => void +} + +class TagMenu extends React.Component { + + menuRef = React.createRef(); + + state = { + left: 20, + top: -1000, + tag: "", + position: -1000, + existingAttrs: {}, + selectedTagKey: "" + }; + + componentDidUpdate(prevProps,prevState) { + const nextStyle = this.calculatePosition(this.props); + if(prevState.top != nextStyle.top){ + this.setState({ top: nextStyle.top, left: nextStyle.left, tag: "", position: nextStyle.position, existingAttrs: nextStyle.existingAttrs, selectedTagIndex: "" }); + } + } + + handleTagChange(event) { + this.setState({tag: event.target.value}) + } + + addTag(event){ + const { view } = this.props; + const { state } = view; + const { tr, selection } = state + const tagName = this.state.tag + var newTags = {} + for (var key in this.state.existingAttrs){ + newTags[key] = this.state.existingAttrs[key] + } + newTags[uuidv4()] = tagName; + const transaction = tr.setNodeMarkup(this.state.position, undefined, {tags: newTags}); + view.dispatch(transaction); + this.setState({tag: "", existingAttrs: newTags}) + } + + deleteTag(keyToDelete){ + const { view } = this.props; + const { state } = view; + const { tr, selection } = state + const tagName = this.state.tag + var newTags = {} + for (var key in this.state.existingAttrs){ + if(key !== keyToDelete){ + newTags[key] = this.state.existingAttrs[key] + } + } + const transaction = tr.setNodeMarkup(this.state.position, undefined, {tags: newTags}); + view.dispatch(transaction); + this.setState({existingAttrs: newTags}) + } + render() { + return ( +
+
+
+ + +
+ +
+
+ ) + } + + getScrollTop() { + return ( + window.pageYOffset || + document.documentElement.scrollTop || + document.body.scrollTop || + 0 + ); + } + + getScrollRight(){ + return ( + window.pageXOffset || + document.documentElement.scrollLeft || + document.body.scrollLeft || + 0 + ); + } + + getOffset(element) { + const rect = element.getBoundingClientRect(); + return { + top: rect.top + this.getScrollTop(), + right: rect.right + } + } + + calculatePosition(props) { + const { view } = props; + const { state } = view; + const { selection } = state; + + if (!selection) { + return { + top: -1000 + } + } + + const { $anchor } = selection; + const resolvedPos = state.doc.resolve($anchor.pos) as any; + const rowNumber = resolvedPos.path[1]; + let i = 0; + if ($anchor.pos === 0) { + return { + top: -1000 + } + } + const [firstNode] = findChildren( + state.doc, + _node => { + if (rowNumber === i || rowNumber + 1 === i) { + i++ + return true + } + i++ + return false + }, + false + ) + if (!firstNode) { + return { + top: -1000 + } + } + + const coords = view.coordsAtPos(firstNode.pos); + const dom = view.nodeDOM(firstNode.pos) as HTMLElement; + const element = this.getOffset(dom); + + if (coords.top === 0) { + return { + top: -1000 + } + } + return { + left: element.right - 250, + top: element.top - 40, + position: firstNode.pos, + existingAttrs: firstNode.node.attrs.tags + } + } +} + +class TagItem extends React.Component { + render() { + return( +
  • +
    +
    + {this.props.tagName} +
    +
    this.props.deleteTag()} + > + +
    +
    +
  • + ) + } +} +export default TagMenu; diff --git a/src/index.tsx b/src/index.tsx index c601f84e9..4df3cb747 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,6 +17,7 @@ import { SearchResult } from "./components/LinkEditor"; import { EmbedDescriptor } from "./types"; import FloatingToolbar from "./components/FloatingToolbar"; import BlockMenu from "./components/BlockMenu"; +import TagMenu from "./components/TagMenu"; import Tooltip from "./components/Tooltip"; import Extension from "./lib/Extension"; import ExtensionManager from "./lib/ExtensionManager"; @@ -63,6 +64,7 @@ import SmartText from "./plugins/SmartText"; import TrailingNode from "./plugins/TrailingNode"; import MarkdownPaste from "./plugins/MarkdownPaste"; import TagFiltering from "./plugins/TagFiltering"; +import SelectedNode from "./plugins/SelectedNode"; export { schema, parser, serializer } from "./server"; @@ -264,6 +266,7 @@ class RichMarkdownEditor extends React.PureComponent { new TrailingNode(), new MarkdownPaste(), new TagFiltering(), + new SelectedNode(), new Keys({ onSave: this.handleSave, onSaveAndExit: this.handleSaveAndExit, @@ -567,6 +570,9 @@ class RichMarkdownEditor extends React.PureComponent { onShowToast={this.props.onShowToast} embeds={this.props.embeds} /> + )} @@ -620,6 +626,10 @@ const StyledEditor = styled("div")<{ readOnly: boolean }>` opacity: 0.5; } + .selectedNode{ + background: #f2f2f4 + } + .ProseMirror-hideselection *::selection { background: transparent; } diff --git a/src/nodes/Blockquote.ts b/src/nodes/Blockquote.ts index d8d12cb8c..92b7bbfb7 100644 --- a/src/nodes/Blockquote.ts +++ b/src/nodes/Blockquote.ts @@ -9,6 +9,11 @@ export default class Blockquote extends Node { get schema() { return { + attrs: { + tags: { + default: {} + } + }, content: "block+", group: "block", parseDOM: [{ tag: "blockquote" }], diff --git a/src/nodes/BulletList.ts b/src/nodes/BulletList.ts index 6c5ce08d8..c9653311d 100644 --- a/src/nodes/BulletList.ts +++ b/src/nodes/BulletList.ts @@ -9,6 +9,11 @@ export default class BulletList extends Node { get schema() { return { + attrs: { + tags: { + default: {} + } + }, content: "list_item+", group: "block", parseDOM: [{ tag: "ul" }], diff --git a/src/nodes/CheckboxItem.ts b/src/nodes/CheckboxItem.ts index 82eef78ec..3c46a2498 100644 --- a/src/nodes/CheckboxItem.ts +++ b/src/nodes/CheckboxItem.ts @@ -20,6 +20,9 @@ export default class CheckboxItem extends Node { checked: { default: false, }, + tags: { + default: {} + } }, content: "paragraph block*", defining: true, diff --git a/src/nodes/CheckboxList.ts b/src/nodes/CheckboxList.ts index 15dcb5129..89a828772 100644 --- a/src/nodes/CheckboxList.ts +++ b/src/nodes/CheckboxList.ts @@ -9,6 +9,11 @@ export default class CheckboxList extends Node { get schema() { return { + attrs: { + tags: { + default: {} + } + }, group: "block", content: "checkbox_item+", toDOM: () => ["ul", { class: this.name }, 0], diff --git a/src/nodes/CodeFence.ts b/src/nodes/CodeFence.ts index 5b9dd1dfd..4d925cd78 100644 --- a/src/nodes/CodeFence.ts +++ b/src/nodes/CodeFence.ts @@ -65,6 +65,9 @@ export default class CodeFence extends Node { language: { default: "javascript", }, + tags: { + default: {} + } }, content: "text*", marks: "", diff --git a/src/nodes/Embed.tsx b/src/nodes/Embed.tsx index 12825ad0e..c58ace56c 100644 --- a/src/nodes/Embed.tsx +++ b/src/nodes/Embed.tsx @@ -14,6 +14,9 @@ export default class Embed extends Node { href: {}, component: {}, matches: {}, + tags: { + default: {} + } }, parseDOM: [{ tag: "iframe" }], toDOM: node => [ diff --git a/src/nodes/Heading.ts b/src/nodes/Heading.ts index e83d870cb..e9a41f189 100644 --- a/src/nodes/Heading.ts +++ b/src/nodes/Heading.ts @@ -30,6 +30,9 @@ export default class Heading extends Node { level: { default: 1, }, + tags: { + default: {} + } }, content: "inline*", group: "block", @@ -46,6 +49,7 @@ export default class Heading extends Node { button.className = "heading-anchor"; button.addEventListener("click", this.handleCopyLink()); // TODO (carl) hiding then unhiding changes h level to 1 somehow + // add the text node to the newly created div if (node.attrs.hidden) { return [ `h${node.attrs.level + (this.options.offset || 0)}`, diff --git a/src/nodes/Image.tsx b/src/nodes/Image.tsx index c6100e52c..d7d82ca4b 100644 --- a/src/nodes/Image.tsx +++ b/src/nodes/Image.tsx @@ -75,6 +75,9 @@ export default class Image extends Node { alt: { default: null, }, + tags:{ + default: {} + } }, content: "text*", marks: "", diff --git a/src/nodes/ListItem.ts b/src/nodes/ListItem.ts index cc833cced..8e78299f0 100644 --- a/src/nodes/ListItem.ts +++ b/src/nodes/ListItem.ts @@ -16,6 +16,9 @@ export default class ListItem extends Node { hidden: { default: false, }, + tags: { + default: {} + } }, content: "paragraph block*", defining: true, diff --git a/src/nodes/OrderedList.ts b/src/nodes/OrderedList.ts index ca889c78e..d34a18931 100644 --- a/src/nodes/OrderedList.ts +++ b/src/nodes/OrderedList.ts @@ -13,6 +13,9 @@ export default class OrderedList extends Node { order: { default: 1, }, + tags: { + default: {} + } }, content: "list_item+", group: "block", diff --git a/src/nodes/Paragraph.ts b/src/nodes/Paragraph.ts index 1391325bb..62652516d 100644 --- a/src/nodes/Paragraph.ts +++ b/src/nodes/Paragraph.ts @@ -12,6 +12,9 @@ export default class Paragraph extends Node { hidden: { default: false, }, + tags: { + default: {} + } }, content: "inline*", group: "block", @@ -25,6 +28,7 @@ export default class Paragraph extends Node { if (node.attrs.hidden) { return ["p", { class: "hidden" }, 0]; } + return ["p", 0]; }, }; diff --git a/src/plugins/SelectedNode.ts b/src/plugins/SelectedNode.ts new file mode 100644 index 000000000..8f78c8c6c --- /dev/null +++ b/src/plugins/SelectedNode.ts @@ -0,0 +1,37 @@ +import { Plugin } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; +import Extension from "../lib/Extension"; + +export default class SelectedNode extends Extension { + get name() { + return "SelectedNode"; + } + + get plugins() { + return [ + new Plugin({ + props: { + decorations(state) { + const { selection } = state; + const decorations = []; + state.doc.nodesBetween( + selection.from, + selection.to, + (node, position) => { + if (node.isBlock) { + decorations.push( + Decoration.node(position, position + node.nodeSize, { + class: 'selectedNode' + }) + ) + } + } + ) + return DecorationSet.create(state.doc, decorations); + } + } + }) + ] + } + +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0d6c8426b..545af193c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -386,10 +386,10 @@ dependencies: "@types/prismjs" "*" -"@types/styled-components@^4.4.2": - version "4.4.3" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.3.tgz#74dd00ad760845a98890a8539361d8afc32059de" - integrity sha512-U0udeNOZBfUkJycmGJwmzun0FBt11rZy08weVQmE2xfUNAbX8AGOEWxWna2d+qAUKxKgMlcG+TZT0+K2FfDcnQ== +"@types/styled-components@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.0.tgz#24d3412ba5395aa06e14fbc93c52f9454cebd0d6" + integrity sha512-ZFlLCuwF5r+4Vb7JUmd+Yr2S0UBdBGmI7ctFTgJMypIp3xOHI4LCFVn2dKMvpk6xDB2hLRykrEWMBwJEpUAUIQ== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" @@ -3407,7 +3407,7 @@ loglevelnext@^1.0.1, loglevelnext@^1.0.2: es6-symbol "^3.1.1" object.assign "^4.1.0" -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -4355,7 +4355,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -4594,16 +4594,6 @@ rc@^1.0.1, rc@^1.1.6: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.8.6: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" - integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" - react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -4621,15 +4611,6 @@ react-portal@^4.2.1: dependencies: prop-types "^15.5.8" -react@^16.8.6: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -4914,14 +4895,6 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"