Skip to content

Commit

Permalink
feat: 🎸 make placeholder of slash plugin configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
Saul-Mirone committed Sep 11, 2021
1 parent bf016ab commit e14eff5
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 167 deletions.
20 changes: 17 additions & 3 deletions packages/plugin-slash/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@ import { AtomList, createProsePlugin, Utils } from '@milkdown/utils';
import { config } from './config';
import { WrappedAction } from './item';
import { createSlashPlugin } from './prose-plugin';
import { CursorStatus } from './prose-plugin/status';

export { config } from './config';
export { CursorStatus } from './prose-plugin/status';
export { createDropdownItem, nodeExists } from './utility';

export type SlashConfig = (utils: Utils) => WrappedAction[];

export { config } from './config';
export const slashPlugin = createProsePlugin<{ config: SlashConfig }>((options, utils) => {
export type Options = {
config: SlashConfig;
placeholder: {
[CursorStatus.Empty]: string;
[CursorStatus.Slash]: string;
};
};
export const slashPlugin = createProsePlugin<Options>((options, utils) => {
const slashConfig = options?.config ?? config;
const placeholder = {
[CursorStatus.Empty]: 'Type / to use the slash commands...',
[CursorStatus.Slash]: 'Type to filter...',
...(options?.placeholder ?? {}),
};
const cfg = slashConfig(utils);

return createSlashPlugin(utils, cfg);
return createSlashPlugin(utils, cfg, placeholder);
});

export const slash = AtomList.create([slashPlugin()]);
49 changes: 49 additions & 0 deletions packages/plugin-slash/src/prose-plugin/dropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* Copyright 2021, Milkdown by Mirone. */
import scrollIntoView from 'smooth-scroll-into-view-if-needed';

import { Action } from '../item';
import { Status } from './status';

export const renderDropdown = (status: Status, dropdownElement: HTMLElement, items: Action[]): boolean => {
const { filter } = status.get();

if (!status.isSlash()) {
dropdownElement.classList.add('hide');
return false;
}

const activeList = items
.filter((item) => {
item.$.classList.remove('active');
const result = item.keyword.some((key) => key.includes(filter.toLocaleLowerCase()));
if (result) {
return true;
}
item.$.classList.add('hide');
return false;
})
.map((item) => {
item.$.classList.remove('hide');
return item;
});

status.setActions(activeList);

if (activeList.length === 0) {
dropdownElement.classList.add('hide');
return false;
}

dropdownElement.classList.remove('hide');

activeList[0].$.classList.add('active');
requestAnimationFrame(() => {
scrollIntoView(activeList[0].$, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
});

return true;
};
6 changes: 3 additions & 3 deletions packages/plugin-slash/src/prose-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { Plugin, PluginKey } from 'prosemirror-state';

import { transformAction, WrappedAction } from '../item';
import { createProps } from './props';
import { createStatus } from './status';
import { createStatus, CursorStatus } from './status';
import { createView } from './view';

export const key = 'MILKDOWN_PLUGIN_SLASH';

export const createSlashPlugin = (utils: Utils, items: WrappedAction[]) => {
export const createSlashPlugin = (utils: Utils, items: WrappedAction[], placeholder: Record<CursorStatus, string>) => {
const status = createStatus();
const actions = items.map(transformAction);

return new Plugin({
key: new PluginKey(key),
props: createProps(status, utils),
props: createProps(status, utils, placeholder),
view: (view) => createView(status, actions, view, utils),
});
};
120 changes: 120 additions & 0 deletions packages/plugin-slash/src/prose-plugin/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/* Copyright 2021, Milkdown by Mirone. */

import { EditorView } from 'prosemirror-view';
import scrollIntoView from 'smooth-scroll-into-view-if-needed';

import { Action } from '../item';
import { Status } from './status';

export const createMouseManager = () => {
let mouseLock = false;

return {
isLock: () => mouseLock,
lock: () => {
mouseLock = true;
},
unlock: () => {
mouseLock = false;
},
};
};
export type MouseManager = ReturnType<typeof createMouseManager>;

export const handleMouseMove = (mouseManager: MouseManager) => () => {
mouseManager.unlock();
};

export const handleMouseEnter = (status: Status, mouseManager: MouseManager) => (e: MouseEvent) => {
if (mouseManager.isLock()) return;
const active = status.get().activeActions.findIndex((x) => x.$.classList.contains('active'));
if (active >= 0) {
status.get().activeActions[active].$.classList.remove('active');
}
const { target } = e;
if (!(target instanceof HTMLElement)) return;
target.classList.add('active');
};

export const handleMouseLeave = () => (e: MouseEvent) => {
const { target } = e;
if (!(target instanceof HTMLElement)) return;
target.classList.remove('active');
};

export const handleClick =
(status: Status, items: Action[], view: EditorView, dropdownElement: HTMLElement) =>
(e: Event): void => {
const { target } = e;
if (!(target instanceof HTMLElement)) return;
if (!view) return;

const stop = () => {
e.stopPropagation();
e.preventDefault();
};

const el = Object.values(items).find(({ $ }) => $.contains(target));
if (!el) {
if (status.isEmpty()) return;

status.clearStatus();
dropdownElement.classList.add('hide');
stop();

return;
}

stop();
el.command(view.state, view.dispatch, view);
};

export const handleKeydown =
(status: Status, view: EditorView, dropdownElement: HTMLElement, mouseManager: MouseManager) => (e: Event) => {
if (!(e instanceof KeyboardEvent)) return;
if (!mouseManager.isLock()) mouseManager.lock();

const { key } = e;
if (!status.isSlash()) return;
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(key)) return;

const { activeActions } = status.get();

let active = activeActions.findIndex(({ $ }) => $.classList.contains('active'));
if (active < 0) active = 0;

const moveActive = (next: number) => {
activeActions[active].$.classList.remove('active');
activeActions[next].$.classList.add('active');
scrollIntoView(activeActions[next].$, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
};

if (key === 'ArrowDown') {
const next = active === activeActions.length - 1 ? 0 : active + 1;

moveActive(next);
return;
}

if (key === 'ArrowUp') {
const next = active === 0 ? activeActions.length - 1 : active - 1;

moveActive(next);
return;
}

if (key === 'Escape') {
if (status.isEmpty()) return;

status.clearStatus();
dropdownElement.classList.add('hide');
return;
}

activeActions[active].command(view.state, view.dispatch, view);
activeActions[active].$.classList.remove('active');
};
6 changes: 3 additions & 3 deletions packages/plugin-slash/src/prose-plugin/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const createSlashStyle = () => css`
}
`;

export const createProps = (status: Status, utils: Utils) => {
export const createProps = (status: Status, utils: Utils, placeholder: Record<CursorStatus, string>) => {
const emptyStyle = utils.getStyle(createEmptyStyle);
const slashStyle = utils.getStyle(createSlashStyle);

Expand Down Expand Up @@ -75,13 +75,13 @@ export const createProps = (status: Status, utils: Utils) => {

if (isEmpty) {
status.clearStatus();
const text = 'Type / to use the slash commands...';
const text = placeholder[CursorStatus.Empty];
return createDecoration(text, [emptyStyle, 'empty-node']);
}

if (isSlash) {
status.setSlash();
const text = 'Type to filter...';
const text = placeholder[CursorStatus.Slash];
return createDecoration(text, [emptyStyle, slashStyle, 'empty-node', 'is-slash']);
}

Expand Down
Loading

0 comments on commit e14eff5

Please sign in to comment.