Skip to content

Commit

Permalink
Release/0.0.1 (#2)
Browse files Browse the repository at this point in the history
* Feature/add library (#1)

---------

Co-authored-by: Krzysztof Nofz <[email protected]>

Co-authored-by: Krzysztof Nofz <[email protected]>
  • Loading branch information
zgrybus and Krzysztof Nofz committed Mar 25, 2024
1 parent 111680f commit 9ed7ce3
Show file tree
Hide file tree
Showing 14 changed files with 3,638 additions and 2 deletions.
46 changes: 46 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
],
"rules": {
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-function": "off",
"import/export": "off",
"import/no-unresolved": "off",
"import/order": [
"error",
{
"newlines-between": "always",
"groups": ["builtin", "external", "index", "internal", ["sibling", "parent"], "object"],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
},
"typescript": {}
}
}
}
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tgz
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"arrowParens": "avoid",
"bracketSameLine": false
}
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,64 @@
# milkdown-mentions-plugin
Plugin that allows you to mentions people and transform it into links
- [Overview](#overview)
- [Using library](#using-library)

## Overview

This library allows you to tag people, for example. **Of course, you can tag anything or anyone you want, because the library provides options to render a list of people or things you want to tag**, as well as the page to which the user will be redirected after clicking on the tag.
**Additionally, the library transforms the tag into a link.**

![Demo](https://github.com/zgrybus/milkdown-mentions-plugin/blob/master/plugin_demo.gif)

## Using library

Using `npm`
```bash
npm i --save milkdown-mentions-plugin
```
Using `yarn`
```bash
yarn add milkdown-mentions-plugin
```

and then import the given components as shown below
```typescript
import { Editor as MilkdownEditor, rootCtx } from '@milkdown/core';
import { commonmark } from '@milkdown/preset-commonmark';
import { useEditor } from '@milkdown/react';
import {
mentionsPlugin,
mentionsPluginOptions,
} from 'milkdown-mentions-plugin';

import { YourComponent } from '../YourComponent';

export const useMilkdown = () => {
const mentions = mentionsPlugin();

const editor = useEditor(
root =>
MilkdownEditor.make()
.config(ctx => {
ctx.set(rootCtx, root);
ctx.update(mentionsPluginOptions.key, prev => ({
...prev,
view: MyMentionsPluginDropdownView,
}));
})
.use(commonmark)
.use(mentions),
[],
);

return editor;
};
```

```typescript
import { MentionsListDropdownProps } from 'milkdown-mentions-plugin'

export const MyMentionsPluginDropdownView: React.FC<MentionsListDropdownProps> = ({ queryText, onMentionItemClick }) => {
return [...my list].filter(text => text.includes(queryText)).map(text => (
<button key={text} onClick={() => onMentionItemClick(text, `https://facebook.com/user/${text}`)}>{text}</button>
))
}
```
54 changes: 54 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "milkdown-mentions-plugin",
"version": "0.0.1",
"type": "module",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/main.d.ts",
"import": "./dist/milkdown-mentions-plugin.js",
"default": "./dist/milkdown-mentions-plugin.js"
}
},
"types": "./dist/main.d.ts",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"@milkdown/core": "^7.3.5",
"@milkdown/ctx": "^7.3.5",
"@milkdown/preset-commonmark": "^7.3.5",
"@milkdown/prose": "^7.3.5",
"@milkdown/react": "^7.3.5",
"@milkdown/transformer": "^7.3.5",
"@prosemirror-adapter/react": "^0.2.6",
"@types/react": "^18.2.67",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.2.2",
"vite": "^5.1.6",
"vite-plugin-dts": "^3.7.3"
},
"peerDependencies": {
"@milkdown/react": ">=7.3.3",
"@milkdown/core": ">=7.3.3",
"@milkdown/preset-commonmark": ">=7.3.3",
"@milkdown/prose": ">=7.3.3",
"@prosemirror-adapter/react": ">=0.2.6",
"react": ">18.0.0",
"react-dom": ">18.0.0"
},
"dependencies": {
"@milkdown/utils": "^7.3.5"
}
}
3 changes: 3 additions & 0 deletions packages/lib/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { mentionsPlugin, mentionsPluginOptions } from './plugin';
export type { MentionsPluginAttrs } from './utils';
export type { MentionsListDropdownProps } from './view/MentionsWidget';
106 changes: 106 additions & 0 deletions packages/lib/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { editorViewCtx } from '@milkdown/core';
import { Plugin, PluginKey } from '@milkdown/prose/state';
import { DecorationSet } from '@milkdown/prose/view';
import { $ctx, $prose } from '@milkdown/utils';
import { useWidgetViewFactory } from '@prosemirror-adapter/react';
import { useMemo } from 'react';

import {
MentionsPluginAttrs,
computeStateFromSelection,
getInitState,
} from './utils';
import {
MentionsListDropdownProps,
MentionsWidget,
} from './view/MentionsWidget';

export type MentionsOptions = {
view?: React.FC<MentionsListDropdownProps>;
};

export const mentionsPluginOptions = $ctx<MentionsOptions, 'mentionsOptions'>(
{},
'mentionsOptions',
);

export const mentionsPlugin = () => {
const widgetViewFactory = useWidgetViewFactory();

const proseMentionsPlugin = useMemo(
() =>
$prose(ctx => {
const key = new PluginKey<MentionsPluginAttrs>('MENTIONS_PLUGIN');
return new Plugin({
key,
state: {
init() {
return getInitState();
},
apply(tr) {
const newState = getInitState();
const { selection } = tr;

if (selection.from !== selection.to) {
return newState;
}

const stateFromSelection = computeStateFromSelection(
ctx,
selection,
);

if (stateFromSelection) {
return stateFromSelection;
}

return newState;
},
},
props: {
decorations(state) {
const newState = key.getState(state);

if (newState?.queryText) {
const { range } = newState;
const editorView = ctx.get(editorViewCtx);

const start = editorView.coordsAtPos(range.from);
const box = editorView.dom.getBoundingClientRect();

const height = start.bottom - start.top;

const left = start.left - box.left;
const top = start.top - box.top + height;

const div = document.createElement('div');
div.style.position = 'absolute';
div.style.inset = '0 auto auto 0';
div.style.transform = `translate(${left}px, ${top}px)`;
div.style.zIndex = '100000';

const createWidget = widgetViewFactory({
as: div,
component: MentionsWidget,
});

return DecorationSet.create(state.tr.doc, [
createWidget(0, newState),
]);
}

return DecorationSet.empty;
},
},
});
}),
[],
);

const mentionsPlugin = useMemo(
() => [mentionsPluginOptions, proseMentionsPlugin],
[],
);

return mentionsPlugin;
};
55 changes: 55 additions & 0 deletions packages/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Ctx } from '@milkdown/ctx';
import { linkSchema } from '@milkdown/preset-commonmark';
import { Selection } from '@milkdown/prose/state';

export type MentionsPluginAttrs = {
range: {
to: number;
from: number;
};
queryText: string | undefined;
};

const mentionsRegex = new RegExp('(^|\\s)@([\\w-\\+]+)$');
export const computeStateFromSelection = (
ctx: Ctx,
selection: Selection,
): MentionsPluginAttrs | undefined => {
const { $from } = selection;

const parastart = $from.before();
const text = $from.doc.textBetween(parastart, $from.pos, '\n', '\0');
const match = text.match(mentionsRegex);

if (match) {
const { index = 0 } = match;
const [value, , queryText] = match;

match.index = value.startsWith(' ') ? index + 1 : match.index;
match[0] = value.trim();

const from = $from.start() + (match.index as number);
const to = from + match[0].length;

const isLink = $from.doc.rangeHasMark(from, to, linkSchema.type(ctx));

if (isLink) {
return undefined;
}

return {
range: { from, to },
queryText: queryText,
};
}

return undefined;
};

export const getInitState = (): MentionsPluginAttrs => ({
range: {
to: 0,
from: 0,
},
queryText: '',
});
Loading

0 comments on commit 9ed7ce3

Please sign in to comment.