Skip to content

Commit

Permalink
feat: add option to remove link
Browse files Browse the repository at this point in the history
  • Loading branch information
sibiraj-s committed Sep 14, 2020
1 parent f061474 commit 87e8d50
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 19 deletions.
24 changes: 20 additions & 4 deletions demo/src/app/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default {
{
type: 'link',
attrs: {
href: 'http://codemirror.net',
href: 'https://codemirror.net',
title: null
}
}
Expand All @@ -56,7 +56,24 @@ export default {
},
{
type: 'text',
text: ' code editor, which provides syntax highlighting, auto-indentation, and similar.'
text: ' code editor, which provides '
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://en.wikipedia.org',
title: ''
}
}
],
text: 'syntax highlighting'
},
{
type: 'text',
text: ', auto-indentation, and similar.'
}
]
},
Expand All @@ -74,8 +91,7 @@ export default {
content: [
{
type: 'text',
text:
'The content of the code editor is kept in sync with the content of the code block in the rich text editor, so that it is as if you\'re directly editing the outer document, using a more convenient interface.'
text: 'The content of the code editor is kept in sync with the content of the code block in the rich text editor, so that it is as if you\'re directly editing the outer document, using a more convenient interface.'
}
]
}
Expand Down
7 changes: 4 additions & 3 deletions demo/src/app/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { menu, placeholder } from 'ngx-editor';

import { undo, redo, history } from 'prosemirror-history';
import { splitListItem, liftListItem, sinkListItem } from 'prosemirror-schema-list';
import { keymap } from 'prosemirror-keymap';
import { toggleMark, baseKeymap } from 'prosemirror-commands';
import { Plugin } from 'prosemirror-state';

import { menu, placeholder, link } from 'ngx-editor';

import codemirrorMenu from './menu/codemirror';
import { buildInputRules } from './input-rules';
import schema from '../schema';
Expand Down Expand Up @@ -72,7 +72,8 @@ const getPlugins = (): Plugin[] => {
blockquote: 'Quote'
}
}),
placeholder('Type Something here...')
placeholder('Type Something here...'),
link(),
];

return plugins;
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@types/node": "^12.11.1",
"@types/prosemirror-commands": "^1.0.3",
"@types/prosemirror-history": "^1.0.1",
"@types/prosemirror-inputrules": "^1.0.2",
"@types/prosemirror-inputrules": "^1.0.3",
"@types/prosemirror-keymap": "^1.0.3",
"@types/prosemirror-model": "^1.7.2",
"@types/prosemirror-schema-basic": "^1.0.1",
Expand All @@ -79,11 +79,11 @@
"prosemirror-history": "^1.1.3",
"prosemirror-inputrules": "^1.1.2",
"prosemirror-keymap": "^1.1.4",
"prosemirror-model": "^1.11.0",
"prosemirror-model": "^1.11.2",
"prosemirror-schema-basic": "^1.1.2",
"prosemirror-schema-list": "^1.1.4",
"prosemirror-state": "^1.3.3",
"prosemirror-view": "^1.15.6",
"prosemirror-view": "^1.15.7",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
Expand Down
61 changes: 60 additions & 1 deletion src/lib/ngx-editor.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ $menubar-text-padding: 0 $menu-item-spacing;
background-clip: padding-box;
border-radius: $border-radius;
border: 2px solid rgba(0, 0, 0, 0.2);
position: relative;
}

.NgxEditor__MenuBar {
Expand All @@ -48,7 +49,10 @@ $menubar-text-padding: 0 $menu-item-spacing;
height: $icon-size;
width: $icon-size;
transition: 0.3s ease-in-out;
margin-right: 2px;

& + #{ $self }--Icon {
margin-left: 2px;
}
}

&#{ $self }--Text {
Expand Down Expand Up @@ -171,6 +175,61 @@ $menubar-text-padding: 0 $menu-item-spacing;
}
}

.NgxEditor__FloatingBubble {
position: absolute;
z-index: 20;
background: white;
border: 1px solid silver;
border-radius: 4px;
padding: 0.3rem;
margin-bottom: 0.3rem;
transform: translateX(-50%);
display: flex;

&::before,
&::after {
content: "";
height: 0;
width: 0;
position: absolute;
left: 50%;
margin-left: -5px;
border: 5px solid transparent;
border-bottom-width: 0;
}

&::before {
bottom: -6px;
border-top-color: silver;
}

&::after {
bottom: -4.5px;
border-top-color: white;
}

a {
display: inline-block;
max-width: 15rem;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 2px;
}

.commands {
display: flex;
margin-left: 0.5rem;

.command {
border: none;

&:hover {
text-decoration: underline;
}
}
}
}

// prosemirror
.ProseMirror {
outline: none;
Expand Down
28 changes: 28 additions & 0 deletions src/lib/prosemirror/helpers/bubblePosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { EditorView } from 'prosemirror-view';

interface TooltipPosition {
bottom: number;
left: number;
}

export const calculateBubblePos = (view: EditorView, toolTipEl: HTMLElement): TooltipPosition => {
const { state: { selection } } = view;
const { from, to } = selection;

// These are in screen coordinates
const start = view.coordsAtPos(from);
const end = view.coordsAtPos(to);

// The box in which the tooltip is positioned, to use as base
const parent = toolTipEl.offsetParent;
const box = parent.getBoundingClientRect();

// Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left)
const left = Math.max((start.left + end.left) / 2, start.left + 3);

return {
left: left - box.left,
bottom: box.bottom - start.top
};
};
16 changes: 16 additions & 0 deletions src/lib/prosemirror/helpers/getSelectionMarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EditorState } from 'prosemirror-state';
import { Mark } from 'prosemirror-model';

export const getSelectionMarks = (state: EditorState): Mark[] => {
let marks: Mark[] = [];

const { selection: { from, to } } = state;

state.doc.nodesBetween(from, to, node => {
marks = [...marks, ...node.marks];
});

return marks;
};

export default getSelectionMarks;
1 change: 1 addition & 0 deletions src/lib/prosemirror/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './isMarkActive';
export * from './isNodeActive';
export * from './isListItem';
export * from './getSelectionMarks';
1 change: 1 addition & 0 deletions src/lib/prosemirror/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as placeholder } from './placeholder';
export { default as menu } from './menu';
export { default as link } from './link';
136 changes: 136 additions & 0 deletions src/lib/prosemirror/plugins/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { EditorView } from 'prosemirror-view';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Mark } from 'prosemirror-model';

import { calculateBubblePos } from '../helpers/bubblePosition';
import { isMarkActive, getSelectionMarks } from '../helpers';

class FloatingOptionsView {
bubbleEL: HTMLElement;

constructor(view: EditorView) {
this.render(view);
this.update(view);
}

render(view: EditorView): void {
this.bubbleEL = document.createElement('div');
this.bubbleEL.className = 'NgxEditor__FloatingBubble';
view.dom.parentNode.appendChild(this.bubbleEL);
}

setDomPosition(view: EditorView): void {
// Otherwise, reposition it and update its content
this.bubbleEL.style.display = '';

const { bottom, left } = calculateBubblePos(view, this.bubbleEL);
this.bubbleEL.style.left = left + 'px';
this.bubbleEL.style.bottom = bottom + 'px';
}

createLinkNode(item: Mark, removeCB: (e: MouseEvent) => void): DocumentFragment {
const el = document.createDocumentFragment();

const link = document.createElement('a');
link.href = item.attrs.href;
link.target = '_blank';
link.innerText = item.attrs.href;
link.title = item.attrs.href;

const commands = document.createElement('div');
commands.classList.add('commands');

const editOpt = document.createElement('button');
editOpt.classList.add('command');
editOpt.textContent = 'Edit';

const removeOpt = document.createElement('button');
removeOpt.classList.add('command');
removeOpt.textContent = 'Remove';

removeOpt.onclick = removeCB;

// commands.appendChild(editOpt);
commands.appendChild(removeOpt);

el.appendChild(link);
el.appendChild(commands);

return el;
}

clearBubbleContent(): void {
this.bubbleEL.textContent = '';
}

hideBubble(): void {
this.bubbleEL.style.display = 'none';
}

update(view: EditorView): void {
const { state, dispatch } = view;
const { selection, schema, doc, tr } = state;

if (!schema.marks.link) {
return;
}

const hasFocus = view.hasFocus();
const isActive = isMarkActive(state, schema.marks.link);
const linkMarks: Mark[] = getSelectionMarks(state).filter(mark => mark.type === schema.marks.link);


// hide for selection and show only for clicks
if (!isActive || linkMarks.length !== 1 || !hasFocus) {
this.hideBubble();
return;
}

const { $head: { pos } } = selection;
const [linkItem] = linkMarks;

this.clearBubbleContent();

const removeCB = (e: MouseEvent) => {
e.preventDefault();

const $pos = doc.resolve(pos);
const linkStart = pos - $pos.textOffset;
const linkEnd = linkStart + $pos.parent.child($pos.index()).nodeSize;

tr.removeMark(linkStart, linkEnd);

dispatch(tr);
view.focus();
};

const el = this.createLinkNode(linkItem, removeCB);
this.bubbleEL.appendChild(el);

// update dom position
this.setDomPosition(view);
}

destroy(): void {
this.bubbleEL.remove();
}
}

function linkPlugin(): Plugin {
return new Plugin({
key: new PluginKey('link'),
view(editorView: EditorView): FloatingOptionsView {
return new FloatingOptionsView(editorView);
},
props: {
handleDOMEvents: {
blur(view): boolean {
view.dispatch(view.state.tr.setMeta('LINK_PLUGIN_EDITOR_BLUR', true));
return false;
}
}
}
});
}

export default linkPlugin;
1 change: 0 additions & 1 deletion src/lib/prosemirror/plugins/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const SELECTED_DROPDOWN_ITEM_CLASSNAME = `${DROPDWON_ITEM_CLASSNAME}--Selected`;
const DROPDOWN_ITEMS = new Map();
DROPDOWN_ITEMS.set('heading', ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);


class DropDownView {
private dropdownGroup: ToolbarDropdownGroupKeys;
private dropdownFields: ToolbarDropdownGroupValues;
Expand Down
Loading

0 comments on commit 87e8d50

Please sign in to comment.