Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions src/components/FloatingToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import getTableRowMenuItems from "../menus/tableRow";
import getTableMenuItems from "../menus/table";
import getFormattingMenuItems from "../menus/formatting";
import LinkEditor, { SearchResult } from "./LinkEditor";
import TagEditor from "./TagEditor";
import Menu from "./Menu";
import isMarkActive from "../queries/isMarkActive";
import getMarkRange from "../queries/getMarkRange";
Expand Down Expand Up @@ -129,8 +130,9 @@ export default class FloatingToolbar extends React.Component<Props> {
const rowIndex = getRowIndex(state.selection);
const isTableSelection = colIndex !== undefined && rowIndex !== undefined;
const link = isMarkActive(state.schema.marks.link)(state);
const range = getMarkRange(selection.$from, state.schema.marks.link);

const tag = isMarkActive(state.schema.marks.tag)(state);
const linkRange = getMarkRange(selection.$from, state.schema.marks.link);
const tagRange = getMarkRange(selection.$from, state.schema.marks.tag);
let items = [];
if (isTableSelection) {
items = getTableMenuItems();
Expand All @@ -155,16 +157,20 @@ export default class FloatingToolbar extends React.Component<Props> {
left={this.state.left}
offset={this.state.offset}
>
{link && range ? (
<LinkEditor
mark={range.mark}
from={range.from}
to={range.to}
{...this.props}
/>
) : (
<Menu items={items} {...this.props} />
)}
{(link && linkRange) ? (<LinkEditor
mark={linkRange.mark}
from={linkRange.from}
to={linkRange.to}
{...this.props}
/>) :
((tag && tagRange) ? (<TagEditor
mark={tagRange.mark}
from={tagRange.from}
to={tagRange.to}
{...this.props}/>)
: (<Menu items={items} {...this.props} />)
)
}
</Wrapper>
</Portal>
);
Expand Down
96 changes: 96 additions & 0 deletions src/components/TagEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from "react";
import { EditorView } from "prosemirror-view";
import { Mark } from "prosemirror-model";
import { TrashIcon } from "outline-icons";
import styled, { withTheme } from "styled-components";
import theme from "../theme";
import Flex from "./Flex";
import Input from "./Input";
import ToolbarButton from "./ToolbarButton";


type Props = {
mark: Mark;
from: number;
to: number;
tooltip: typeof React.Component;
view: EditorView;
theme: typeof theme;
};

class TagEditor extends React.Component<Props>{
discardInputValue = false;
initialValue: string = this.props.mark.attrs.name;
state = {
value: this.props.mark.attrs.name,
};

handleChange = async (event): Promise<void> => {
const value = event.target.value.trim();
this.setState({value});
};

componentWillUnmount = () => {
// If we discarded the changes then nothing to do or already saved
if (this.discardInputValue || this.state.value === this.initialValue) {
return;
}
// If the name is empty remove the mark
let name = (this.state.value || "").trim();
if (!name) {
return this.handleRemoveTag();
}
//Save the changes
this.save(name);
};

save = (name: string): void => {
this.discardInputValue = true;
const { from, to } = this.props;
const { state, dispatch } = this.props.view;
const markType = state.schema.marks.tag;

dispatch(
state.tr
.removeMark(from, to, markType)
.addMark(from, to, markType.create({ name }))
);
};

handleRemoveTag = (): void => {
this.discardInputValue = true;
const { from, to, mark } = this.props;
const { state, dispatch } = this.props.view;

dispatch(state.tr.removeMark(from, to, mark));
};

render() {
const { mark } = this.props;
const Tooltip = this.props.tooltip;

return (
<Wrapper>
<Input
value={this.state.value}
placeholder="Search or paste a link…"
onChange={this.handleChange}
autoFocus={mark.attrs.name === ""}
/>
<ToolbarButton onClick={this.handleRemoveTag}>
<Tooltip tooltip="Remove Tag" placement="top">
<TrashIcon color={this.props.theme.toolbarItem} />
</Tooltip>
</ToolbarButton>
</Wrapper>
);
}
}

const Wrapper = styled(Flex)`
margin-left: -8px;
margin-right: -8px;
min-width: 336px;
`;

export default withTheme(TagEditor);
11 changes: 11 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import Highlight from "./marks/Highlight";
import Italic from "./marks/Italic";
import Link from "./marks/Link";
import Strikethrough from "./marks/Strikethrough";
import Tag from "./marks/Tag";
import Hide from "./marks/Hide";

// plugins
import BlockMenuTrigger from "./plugins/BlockMenuTrigger";
Expand Down Expand Up @@ -251,6 +253,8 @@ class RichMarkdownEditor extends React.PureComponent<Props, State> {
new Bold(),
new Code(),
new Highlight(),
new Tag(),
new Hide(),
new Italic(),
new Link({
onClickLink: this.props.onClickLink,
Expand Down Expand Up @@ -742,6 +746,13 @@ const StyledEditor = styled("div")<{ readOnly: boolean }>`
font-style: italic;
}

tag {
background: yellow;
}

hide {
display: none;
}
b,
strong {
font-weight: 600;
Expand Down
28 changes: 28 additions & 0 deletions src/marks/Hide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import markInputRule from "../lib/markInputRule";
import Mark from "./Mark";

export default class Hide extends Mark {
get name() {
return "hide";
}

get schema() {
return {
parseDOM: [{ tag: "hide" }],
toDOM: () => ["hide"],
};
}

get toMarkdown() {
return {
open: "++",
close: "++",
mixable: true,
expelEnclosingWhitespace: true,
};
}

parseMarkdown() {
return { mark: "hide" };
}
}
76 changes: 76 additions & 0 deletions src/marks/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Mark from "./Mark";
import { InputRule } from "prosemirror-inputrules";
import { toggleMark } from "prosemirror-commands";

const TAG_REGEX = /:(.+):\(([^\)]+)\)/;

export default class Tag extends Mark {
get name() {
return "tag";
}

get schema() {
return {
attrs: {
name: {
default: null,
},
},
parseDOM: [
{
tag: "tag[name]",
getAttrs: (dom: HTMLElement) => ({
name: dom.getAttribute("name"),
}),
},
],
toDOM: node => [
"tag",
{
...node.attrs,
},
0,
],
};
}


inputRules({ type }) {
return [
new InputRule(TAG_REGEX, (state, match, start, end) => {
const [okay, name, content] = match;
const { tr } = state;

if (okay) {
tr.replaceWith(start, end, this.editor.schema.text(content)).addMark(
start,
start+content.length,
type.create({ name })
);
}

return tr;
}),
];
}

get toMarkdown() {
return {
open(state, mark, parent, index) {
return ":" + state.esc(mark.attrs.name) + ":("
},
close(state, mark, parent, index) {
return ")"
},
};
}

parseMarkdown() {
return {
mark: "tag",
getAttrs: tok => ({
name: tok.attrGet("name"),
}),
};
}
}
Loading