Skip to content

Commit

Permalink
feat(prosemirror-suggest): add / remove suggester (#428)
Browse files Browse the repository at this point in the history
  • Loading branch information
ifiokjr committed Jul 31, 2020
1 parent e66f8ac commit 068d2e0
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .changeset/hot-schools-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'prosemirror-suggest': minor
---

Allow runtime additions to the `prosemirror-suggest` plugin.

You can now add suggester configurations to active suggest plugin instances. The name is used as an identifier and identical names will automatically be replaced.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEditor, doc, p } from 'jest-prosemirror';

import { ExitReason } from '../suggest-constants';
import { suggest } from '../suggest-plugin';
import { addSuggester, suggest } from '../suggest-plugin';
import { SuggestExitHandlerParameter, SuggestKeyBindingParameter } from '../suggest-types';

describe('Suggest Handlers', () => {
Expand Down Expand Up @@ -184,3 +184,41 @@ describe('Suggest Ignore', () => {
});
});
});

test('addSuggester', () => {
const handlers = {
onExit: jest.fn(),
onChange: jest.fn(),
};

const plugin = suggest();
const editor = createEditor(doc(p('<cursor>')), { plugins: [plugin] });

const remove = addSuggester(editor.view.state, {
char: '@',
name: 'at',
...handlers,
matchOffset: 0,
});

editor
.insertText('@')
.callback(() => {
expect(handlers.onChange).toHaveBeenCalledTimes(1);
})
.insertText('suggest ');

expect(handlers.onExit).toHaveBeenCalledTimes(1);
remove();

jest.clearAllMocks();

editor
.insertText('@')
.callback(() => {
expect(handlers.onChange).not.toHaveBeenCalled();
})
.insertText('suggest ');

expect(handlers.onExit).not.toHaveBeenCalled();
});
24 changes: 24 additions & 0 deletions packages/prosemirror-suggest/src/suggest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ export function getSuggestPluginState<Schema extends EditorSchema = any>(
return getPluginState<SuggestState>(suggestPluginKey, state);
}

/**
* Add a new suggester or replace it if the name already exists in the existing
* configuration.
*
* Will return a function for disposing of the added suggester.
*/
export function addSuggester<Schema extends EditorSchema = any>(
state: EditorState<Schema>,
suggester: Suggestion,
) {
return getSuggestPluginState(state).addSuggester(suggester);
}

/**
* Remove a suggester if it exists. Pass in the name or the full suggester
* object.
*/
export function removeSuggester<Schema extends EditorSchema = any>(
state: EditorState<Schema>,
suggester: Suggestion | string,
) {
return getSuggestPluginState(state).removeSuggester(suggester);
}

/**
* This creates a suggestion plugin with all the suggesters provided.
*
Expand Down
73 changes: 52 additions & 21 deletions packages/prosemirror-suggest/src/suggest-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import mergeDescriptors from 'merge-descriptors';
import { Transaction } from 'prosemirror-state';
import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';

import { bool, isFunction, object } from '@remirror/core-helpers';
import { bool, isFunction, isString, object } from '@remirror/core-helpers';
import {
CompareStateParameter,
EditorSchema,
Expand Down Expand Up @@ -46,7 +46,7 @@ export class SuggestState {
/**
* The suggesters that have been registered for the suggesters plugin.
*/
private readonly suggesters: Array<Required<Suggestion>>;
#suggesters: Array<Required<Suggestion>>;

/**
* Keeps track of the current state.
Expand Down Expand Up @@ -104,23 +104,8 @@ export class SuggestState {
* prioritized.
*/
constructor(suggesters: Suggestion[]) {
const names: string[] = [];
this.suggesters = suggesters.map((suggester) => {
if (names.includes(suggester.name)) {
throw new Error(
`A suggester already exists with the name '${suggester.name}'. The name provided must be unique.`,
);
}

const clone = { ...DEFAULT_SUGGESTER, ...suggester };

// Preserve any descriptors (getters and setters)
mergeDescriptors(clone, suggester);

names.push(suggester.name);

return clone;
});
const mapper = createSuggesterMapper();
this.#suggesters = suggesters.map(mapper);
}

/**
Expand Down Expand Up @@ -278,7 +263,7 @@ export class SuggestState {
*/
addIgnored = ({ from, char, name, specific = false }: AddIgnoredParameter) => {
const to = from + char.length;
const suggester = this.suggesters.find((value) => value.name === name);
const suggester = this.#suggesters.find((value) => value.name === name);

if (!suggester) {
throw new Error(`No suggester exists for the name provided: ${name}`);
Expand Down Expand Up @@ -358,13 +343,39 @@ export class SuggestState {
* Update the next state value.
*/
private updateReasons({ $pos, state }: UpdateReasonsParameter) {
const match = findFromSuggestions({ suggesters: this.suggesters, $pos });
const match = findFromSuggestions({ suggesters: this.#suggesters, $pos });
this.next = match && this.shouldIgnoreMatch(match) ? undefined : match;

// Store the matches with reasons
this.handlerMatches = findReason({ next: this.next, prev: this.prev, state, $pos });
}

/**
* Add a new suggest or replace it if it already exists.
*/
addSuggester(suggester: Suggestion) {
const previous = this.#suggesters.find((item) => item.name === suggester.name);
const mapper = createSuggesterMapper();

if (previous) {
this.#suggesters = this.#suggesters.map((item) =>
item === previous ? mapper(suggester) : item,
);
} else {
this.#suggesters = [...this.#suggesters, mapper(suggester)];
}

return () => this.removeSuggester(suggester.name);
}

/**
* Remove a suggester if it exists.
*/
removeSuggester(suggester: Suggestion | string): void {
const name = isString(suggester) ? suggester : suggester.name;
this.#suggesters = this.#suggesters.filter((item) => item.name !== name);
}

/**
* Used to handle the view property of the plugin spec.
*/
Expand Down Expand Up @@ -503,3 +514,23 @@ interface UpdateReasonsParameter<Schema extends EditorSchema = any>
export interface SuggestStateApplyParameter<Schema extends EditorSchema = any>
extends TransactionParameter<Schema>,
CompareStateParameter<Schema> {}

function createSuggesterMapper() {
const names = new Set<string>();

return (suggestion: Suggestion) => {
if (names.has(suggestion.name)) {
throw new Error(
`A suggestion already exists with the name '${suggestion.name}'. The name provided must be unique.`,
);
}

const clone = { ...DEFAULT_SUGGESTER, ...suggestion };

// Preserve any descriptors (getters and setters)
mergeDescriptors(clone, suggestion);

names.add(suggestion.name);
return clone;
};
}

1 comment on commit 068d2e0

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Published on https://remirror.io as production
🚀 Deployed on https://5f23f04ed1782601b7663ca8--remirror.netlify.app

Please sign in to comment.