diff --git a/README.md b/README.md index db6a7cb..a56048d 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ npm install ng-milkdown ng-prosemirror-adapter @milkdown/core @milkdown/ctx @mil ```html void 0` | -| `[plugins]` | milkdown plugin to use | `NgMilkdownPlugin[]` | `[]` | -| `[loading]` | set the loading status of editor | `boolean` | `true` | -| `[spinner]` | Custom spinner | `TemplateRef` | - | -| `[ngModel]` | current value , double binding | `DefaultValue` | - | -| `(ngModelChange)` | callback when markdown change | `EventEmitter` | - | -| `(onReady)` | A callback function, can be executed when editor has bean created | `Editor` | - | +| Property | Description | Type | Default | +|-------------------|-------------------------------------------------------------------|---------------------------|------------------------| +| `[classList]` | editor element class names | `string[]` | `[]` | +| `[config]` | config before Editor.create() | `NgMilkdownEditorConfig` | `(ctx: Ctx) => void 0` | +| `[plugins]` | milkdown plugin to use | `NgMilkdownPlugin[]` | `[]` | +| `[editor]` | pass in a fully controlled editor object | `(HTMLElement) => Editor` | `[]` | +| `[loading]` | set the loading status of editor | `boolean` | `true` | +| `[spinner]` | custom spinner | `TemplateRef` | - | +| `[ngModel]` | current value , double binding | `DefaultValue` | - | +| `(ngModelChange)` | callback when markdown change | `EventEmitter` | - | +| `(onReady)` | A callback function, can be executed when editor has bean created | `Editor` | - | ## OutOfBox Plugins diff --git a/projects/ng-milkdown/README.md b/projects/ng-milkdown/README.md index 6d950d6..8b555a3 100644 --- a/projects/ng-milkdown/README.md +++ b/projects/ng-milkdown/README.md @@ -1,24 +1,256 @@ -# NgMilkdown +

+ + + +

-This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.0. +

+NG-MILKDOWN +

-## Code scaffolding -Run `ng generate component component-name --project ng-milkdown` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ng-milkdown`. -> Note: Don't forget to add `--project ng-milkdown` or else it will be added to the default project in your `angular.json` file. +WYSIWYG markdown Editor 🍼 [**Milkdown**](https://github.com/Milkdown/milkdown) for [**Angular**](https://angular.dev/) out of box, only supports Angular **17**+. -## Build +## Example -Run `ng build ng-milkdown` to build the project. The build artifacts will be stored in the `dist/` directory. +You can run this example by: -## Publishing +```bash +git clone https://github.com/ousc/ng-milkdown.git +cd ng-milkdown +npm install +npm run start +``` -After building your library with `ng build ng-milkdown`, go to the dist folder `cd dist/ng-milkdown` and run `npm publish`. +## Online Demo -## Running unit tests +[https://ousc.github.io/ng-milkdown](https://ousc.github.io/ng-milkdown) -Run `ng test ng-milkdown` to execute the unit tests via [Karma](https://karma-runner.github.io). +## ng-prosemirror-adapter -## Further help +Angular adapter for ProseMirror, only supports Angular 17+.(now this library is not published to npm, we will publish it soon) -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. +[https://github.com/ousc/ng-prosemirror-adapter](https://github.com/ousc/ng-prosemirror-adapter) + +## Official plugins support on NgMilkdown: + +- [X] `theme-nord`**(preset)** +- [X] `preset-commonmark`**(preset)** +- [X] `plugin-listener`**(preset)** +- [X] `preset-gfm`**(supported)** +- [X] `plugin-history`**(supported)** +- [X] `plugin-prism`**(supported)** +- [X] `plugin-clipboard`**(supported)** +- [X] `plugin-cursor`**(supported)** +- [X] `plugin-math`**(supported)** +- [X] `plugin-block`**(supported)** +- [X] `plugin-indent`**(supported)** +- [X] `plugin-tooltip`**(supported)** +- [X] `plugin-slash`**(supported)** +- [X] `plugin-diagram`**(supported)** +- [X] `plugin-emoji`**(supported)** +- [X] `plugin-cursor`**(supported)** +- [X] `plugin-trailing`**(supported)** +- [X] `plugin-upload`**(supported)** +- [X] `plugin-collab`**(supported)** +- [ ] `plugin-copilot`**(planned)** + +usage of plugins can be found in [example](https://github.com/ousc/ng-milkdown/tree/main/src/app/components); + +## Quick Start + +### Install + +```bash +npm install ng-milkdown ng-prosemirror-adapter @milkdown/core @milkdown/ctx @milkdown/plugin-listener @milkdown/preset-commonmark @milkdown/theme-nord +``` + +### Example + +#### workGround.component.html +```html + + + +``` +#### workGround.component.ts + +```typescript +import {NgMilkdownProvider} from "./ng-milkdown-provider.component"; + +@Component({...}) +export class WorkGroundComponent { + @ViewChild(NgMilkdownProvider, {static: true}) provider: NgMilkdownProvider; + + config = (ctx: any) => { + ctx.set(editorViewOptionsCtx, { + attributes: { + class: "prose dark:prose-invert outline-none mx-auto px-2 py-4 box-border milkdown-theme-nord editor", + spellcheck: "false", + }, + }); + } + + tooltip = tooltipFactory('my-tooltip') + slash = slashFactory('my-slash') + plugins: NgMilkdownPlugin[] = [ + gfm, + history, + prism, + clipboard, + cursor, + math, + emoji, + [ + diagram, // diagram plugin + $view(diagramSchema.node, () => + this.provider.createNodeView({ // create node view for diagram node + component: Diagram, + stopEvent: () => true, + }) + ) + ].flat(), + $view(listItemSchema.node, () => + this.provider.createNodeView({component: ListItem}) // create node view for list item node + ), + { + plugin: block, + config: ctx => { + ctx.set(block.key, { + view: this.provider.createPluginView({ // create plugin view for block plugin + component: BlockComponent, + inputs: {ctx} + }) + }); + } + }, + { + plugin: indent, + config: ctx => { + ctx.set(indentConfig.key as any, { // set indent config + type: 'space', + size: 4, + }); + } + }, + { + plugin: this.tooltip, + config: ctx => { + ctx.set(this.tooltip.key, { + view: this.provider.createPluginView({component: ImageTooltipComponent}) // create plugin view for tooltip plugin + }) + } + }, + { + plugin: this.slash, + config: ctx => { + ctx.set(this.slash.key, { + view: this.provider.createPluginView({component: SlashComponent}) // create plugin view for slash plugin + }) + } + } + ]; + + value = 'Hello, World!'; + + editor: Editor; + + onChange(markdownText: string) { + console.log({markdownText}); + } +} + +``` + +### API + +| Property | Description | Type | Default | +|-------------------|-------------------------------------------------------------------|---------------------------|------------------------| +| `[classList]` | editor element class names | `string[]` | `[]` | +| `[config]` | config before Editor.create() | `NgMilkdownEditorConfig` | `(ctx: Ctx) => void 0` | +| `[plugins]` | milkdown plugin to use | `NgMilkdownPlugin[]` | `[]` | +| `[editor]` | pass in a fully controlled editor object | `(HTMLElement) => Editor` | `[]` | +| `[loading]` | set the loading status of editor | `boolean` | `true` | +| `[spinner]` | custom spinner | `TemplateRef` | - | +| `[ngModel]` | current value , double binding | `DefaultValue` | - | +| `(ngModelChange)` | callback when markdown change | `EventEmitter` | - | +| `(onReady)` | A callback function, can be executed when editor has bean created | `Editor` | - | + +## OutOfBox Plugins + +### ng-milkdown-tooltip + +```typescript +@Component({ + template: ` + + `, + ... +}) +export class ImageTooltipComponent extends NgMilkdownTooltip { + setBold(e: MouseEvent) { + e.preventDefault(); + this.action(callCommand(toggleStrongCommand.key)); + } +} +``` + +### ng-milkdown-slash + +```typescript +@Component({ + template: ` + + `, + ... +}) +export class SlashComponent extends NgMilkdownSlash { + override get onPick(): (ctx: Ctx) => void { + return (ctx: Ctx) => { + this.removeSlash(ctx); + ctx.get(commandsCtx).call(createCodeBlockCommand.key); + ctx.get(editorViewCtx).focus(); + } + } +} +``` + +### ng-milkdown-block + +```typescript +@Component({ + selector: 'block', + template: ` +
+ + + +
+ `, + styles:[], + standalone: true +}) +export class BlockComponent extends NgMilkdownBlock {} +``` + +More detailed examples and more plugins can be found in [example](https://github.com/ousc/ng-milkdown/tree/main/src/app/components); + +## license + +[MIT](./LICENSE) diff --git a/projects/ng-milkdown/src/lib/ng-milkdown.component.ts b/projects/ng-milkdown/src/lib/ng-milkdown.component.ts index 89a58fb..9f3d764 100644 --- a/projects/ng-milkdown/src/lib/ng-milkdown.component.ts +++ b/projects/ng-milkdown/src/lib/ng-milkdown.component.ts @@ -80,22 +80,15 @@ export class NgMilkdown extends NgProsemirrorEditor implements ControlValueAcces @Input() spinner: TemplateRef = null; - get editor() { - return this.ngMilkdownService.editor + get editor(): Editor { + return this.ngMilkdownService.editor; } - set editor(value) { - this.ngMilkdownService.editor = value - } - - _userEditor: (element: HTMLElement) => Promise = null; - - @Input() set userEditor(value: (element: HTMLElement) => Promise) { - this._userEditor = value; - } + _editorFn: (element: HTMLElement) => Promise = null; - get userEditor() { - return this._userEditor; + @Input() + set editor(editorFn: ((element: HTMLElement) => Promise)) { + this._editorFn = editorFn; } writeValue(value: any): void { @@ -124,7 +117,7 @@ export class NgMilkdown extends NgProsemirrorEditor implements ControlValueAcces @Output() onChanged = new EventEmitter(); @Input() plugins: NgMilkdownPlugin[] = []; - @Input() editorConfig: NgMilkdownEditorConfig = (ctx, provider) => null; + @Input() config: NgMilkdownEditorConfig = (ctx, provider) => null; @Output() onReady = new EventEmitter(); onTouched: () => void = () => { }; @@ -138,8 +131,8 @@ export class NgMilkdown extends NgProsemirrorEditor implements ControlValueAcces disabled = false; async render(): Promise { - if (this.userEditor) { - this.editor = await this.userEditor(this.editorRef.nativeElement); + if (this._editorFn) { + this.ngMilkdownService.editor = await this._editorFn(this.editorRef.nativeElement); this.onReady.emit(this.editor); return; } @@ -154,10 +147,10 @@ export class NgMilkdown extends NgProsemirrorEditor implements ControlValueAcces this.onChange(md); }); - if (isZoneAwarePromise(this.editorConfig)) { - await this.editorConfig(ctx, this.provider); + if (isZoneAwarePromise(this.config)) { + await this.config(ctx, this.provider); } else { - this.editorConfig(ctx, this.provider); + this.config(ctx, this.provider); } }) .config(nord) @@ -185,7 +178,7 @@ export class NgMilkdown extends NgProsemirrorEditor implements ControlValueAcces this.loading = false; this.loadingChange.emit(false); this.onReady.emit(editor); - this.editor = editor; + this.ngMilkdownService.editor = editor; }) } diff --git a/src/app/components/copilot/copilot.service.ts b/src/app/components/copilot/copilot.service.ts index 807a55f..765dc8e 100644 --- a/src/app/components/copilot/copilot.service.ts +++ b/src/app/components/copilot/copilot.service.ts @@ -12,6 +12,7 @@ export class CopilotService { constructor(private http: HttpClient) { } + enabled = false; initialState = { deco: DecorationSet.empty, @@ -39,6 +40,7 @@ export class CopilotService { key: this.copilotPluginKey, props: { handleKeyDown: (view, event) => { + if (!this.enabled) return; this.keyDownHandler(ctx, event); }, decorations:(state)=> { @@ -50,6 +52,7 @@ export class CopilotService { return {...this.initialState}; }, apply:(tr, value, _prevState, state)=> { + if (!this.enabled) return value; const message = tr.getMeta(this.copilotPluginKey); console.log(message) if (typeof message !== 'string') return value; @@ -124,7 +127,7 @@ export class CopilotService { } fetchAIHint(prompt: string) { - return this.http.post('https://api.openai.com/v1/completions', { + return this.http.post(localStorage.getItem('openai-api-url'), { model: 'davinci', prompt, "max_tokens": 7, @@ -135,7 +138,7 @@ export class CopilotService { "logprobs": null }, { headers: { - Authorization: 'Bearer ' + localStorage.getItem('token') + Authorization: 'Bearer ' + localStorage.getItem('openai-api-token') } }); } diff --git a/src/app/components/tool-bar.component.ts b/src/app/components/tool-bar.component.ts index 9dbd743..d73c6a6 100644 --- a/src/app/components/tool-bar.component.ts +++ b/src/app/components/tool-bar.component.ts @@ -17,6 +17,7 @@ import {insertDiagramCommand} from "@milkdown/plugin-diagram"; import {CmdKey} from "@milkdown/core"; import {RouterLink} from "@angular/router"; import {NgMilkdownProvider} from "../../../projects/ng-milkdown/src/lib/component/ng-milkdown-provider.component"; +import {CopilotService} from "./copilot/copilot.service"; @Component({ selector: 'tool-bar', @@ -24,15 +25,18 @@ import {NgMilkdownProvider} from "../../../projects/ng-milkdown/src/lib/componen
@for (item of navBarItems;track $index) { -
- {{ item.icon }} -
+ @if (!item.hidden || !item.hidden()) { +
+ {{ item.icon }} +
+ } }
@@ -58,6 +62,9 @@ import {NgMilkdownProvider} from "../../../projects/ng-milkdown/src/lib/componen export class ToolBarComponent { @Input() provider: NgMilkdownProvider; + constructor(private copilotService: CopilotService) { + } + get action() { return actionFactory(this.provider.editor); } @@ -151,12 +158,28 @@ export class ToolBarComponent { icon: 'partner_exchange', className: ['hidden-sm'], routerLink: ['/collaborative-editing'] + }, + { + title: 'Open copilot', + icon: 'smart_toy', + className: ['hidden-sm'], + click: () => this.copilotService.enabled = true, + hidden: () => this.copilotService.enabled + }, + { + title: 'Close copilot', + icon: 'smart_toy', + className: ['hidden-sm', 'text-red-500'], + click: () => this.copilotService.enabled = false, + hidden: () => !this.copilotService.enabled } ] - onMouseDown(e: MouseEvent | TouchEvent, slice: CmdKey, payload?: any) { + onMouseDown(e: MouseEvent | TouchEvent, slice: CmdKey, payload?: any, click?: () => void) { e.preventDefault(); if (slice) this.action(callCommand(slice, payload)); + if (click) + click(); } } diff --git a/src/app/routes/collaborative-editing/collaborative-editing.component.ts b/src/app/routes/collaborative-editing/collaborative-editing.component.ts index b0fb8b7..2c5ea1a 100644 --- a/src/app/routes/collaborative-editing/collaborative-editing.component.ts +++ b/src/app/routes/collaborative-editing/collaborative-editing.component.ts @@ -60,7 +60,7 @@ export interface DialogData {