Skip to content

Commit

Permalink
feat(extension-event): add events for double and triple clicks (#1861)
Browse files Browse the repository at this point in the history
  • Loading branch information
ocavue committed Sep 19, 2022
1 parent 33b4333 commit 2e19230
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-mugs-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@remirror/extension-events': minor
---

Adds four new events `doubleClick`, `doubleClickMark`, `tripleClick` and `tripleClickMark`. They have the same interface as the existing `click` and `clickMark` event, but are triggered when the user double or triple clicks.
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,31 @@ describe('events', () => {
expect(mouseUpHandler).toHaveBeenCalled();
});

it('responds to editor `click` events', () => {
const eventsExtension = new EventsExtension();
const clickHandler: any = jest.fn(() => true);
const editor = renderEditor([eventsExtension]);
const { view, add } = editor;
const { doc, p } = editor.nodes;
const node = p('first');

eventsExtension.addHandler('click', clickHandler);
add(doc(p('first')));

// JSDOM doesn't pass events through so the only way to simulate is to
// directly simulate the `handleClick` prop.
view.someProp('handleClickOn', (fn) => fn(view, 2, node, 1, {} as MouseEvent, true));
expect(clickHandler).toHaveBeenCalled();
});
it.each([
['click', 'handleClickOn'],
['doubleClick', 'handleDoubleClickOn'],
['tripleClick', 'handleTripleClickOn'],
] as const)(
'responds to editor `%s` events',
(remirrorEventName, prosemirrorEventHandlerName) => {
const eventsExtension = new EventsExtension();
const clickHandler: any = jest.fn(() => true);
const editor = renderEditor([eventsExtension]);
const { view, add } = editor;
const { doc, p } = editor.nodes;
const node = p('first');

eventsExtension.addHandler(remirrorEventName, clickHandler);
add(doc(p('first')));

// JSDOM doesn't pass events through so the only way to simulate is to
// directly simulate the `handleClick` prop.
view.someProp(prosemirrorEventHandlerName, (fn) =>
fn(view, 2, node, 1, {} as MouseEvent, true),
);
expect(clickHandler).toHaveBeenCalled();
},
);

it('should not throw when clicked on before first state called', () => {
const eventsExtension = new EventsExtension();
Expand Down
167 changes: 121 additions & 46 deletions packages/remirror__extension-events/src/events-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
NodeWithPosition,
noop,
PlainExtension,
ProsemirrorNode,
range,
ResolvedPos,
} from '@remirror/core';
Expand Down Expand Up @@ -137,6 +138,26 @@ export interface EventsOptions {
*/
clickMark?: Handler<ClickMarkEventHandler>;

/**
* Same as {@link EventsOptions.click} but for double clicks.
*/
doubleClick?: Handler<ClickEventHandler>;

/**
* Same as {@link EventsOptions.clickMark} but for double clicks.
*/
doubleClickMark?: Handler<ClickMarkEventHandler>;

/**
* Same as {@link EventsOptions.click} but for triple clicks.
*/
tripleClick?: Handler<ClickEventHandler>;

/**
* Same as {@link EventsOptions.clickMark} but for triple clicks.
*/
tripleClickMark?: Handler<ClickMarkEventHandler>;

/**
* Listen for contextmenu events and pass through props which detail the
* direct node and parent nodes which were activated.
Expand Down Expand Up @@ -195,6 +216,10 @@ export type HoverEventHandler = (
'keydown',
'click',
'clickMark',
'doubleClick',
'doubleClickMark',
'tripleClick',
'tripleClickMark',
'contextmenu',
'hover',
'scroll',
Expand All @@ -208,6 +233,8 @@ export type HoverEventHandler = (
mouseleave: { earlyReturnValue: true },
mouseup: { earlyReturnValue: true },
click: { earlyReturnValue: true },
doubleClick: { earlyReturnValue: true },
tripleClick: { earlyReturnValue: true },
hover: { earlyReturnValue: true },
contextmenu: { earlyReturnValue: true },
scroll: { earlyReturnValue: true },
Expand Down Expand Up @@ -275,6 +302,68 @@ export class EventsExtension extends PlainExtension<EventsOptions> {
// the reference to the event is lost.
const eventMap: WeakMap<Event, boolean> = new WeakMap();

const runClickHandlerOn = (
clickMark: ClickMarkEventHandler,
click: ClickEventHandler,
// The following arguments are passed through from ProseMirror
// handleClickOn / handleDoubleClickOn / handleTripleClickOn handler
view: EditorView,
pos: number,
node: ProsemirrorNode,
nodePos: number,
event: MouseEvent,
direct: boolean,
) => {
const state = this.store.currentState;
const { schema, doc } = state;
const $pos = doc.resolve(pos);

// True when the event has already been handled. In these cases we
// should **not** run the `clickMark` handler since all that is needed
// is the `$pos` property to check if a mark is active.
const handled = eventMap.has(event);

// Generate the base state which is passed to the `clickMark` handler
// and used to create the `click` handler state.
const baseState = createClickMarkState({ $pos, handled, view, state });
let returnValue = false;

if (!handled) {
// The boolean return value for the mark click handler. This is
// intentionally separate so that both the `clickMark` handlers and
// the `click` handlers are run for each click. It uses the eventMap
// to limit the ensure that it is only run once per click since this
// method is run with the same event for every single node in the
// `doc` tree.
returnValue = clickMark(event, baseState) || returnValue;
}

// Create click state to help API consumers inspect whether the event
// is a relevant click type.
const clickState: ClickHandlerState = {
...baseState,
pos,
direct,
nodeWithPosition: { node, pos: nodePos },

getNode: (nodeType) => {
const type = isString(nodeType) ? schema.nodes[nodeType] : nodeType;

invariant(type, {
code: ErrorConstant.EXTENSION,
message: 'The node being checked does not exist',
});

return type === node.type ? { node, pos: nodePos } : undefined;
},
};

// Store this event so that marks aren't re-run for identical events.
eventMap.set(event, true);

return click(event, clickState) || returnValue;
};

return {
props: {
handleKeyPress: (_, event) => {
Expand All @@ -287,54 +376,40 @@ export class EventsExtension extends PlainExtension<EventsOptions> {
return this.options.textInput({ from, to, text }) || false;
},
handleClickOn: (view, pos, node, nodePos, event, direct) => {
const state = this.store.currentState;
const { schema, doc } = state;
const $pos = doc.resolve(pos);

// True when the event has already been handled. In these cases we
// should **not** run the `clickMark` handler since all that is needed
// is the `$pos` property to check if a mark is active.
const handled = eventMap.has(event);

// Generate the base state which is passed to the `clickMark` handler
// and used to create the `click` handler state.
const baseState = createClickMarkState({ $pos, handled, view, state });
let returnValue = false;

if (!handled) {
// The boolean return value for the mark click handler. This is
// intentionally separate so that both the `clickMark` handlers and
// the `click` handlers are run for each click. It uses the eventMap
// to limit the ensure that it is only run once per click since this
// method is run with the same event for every single node in the
// `doc` tree.
returnValue = this.options.clickMark(event, baseState) || returnValue;
}

// Create click state to help API consumers inspect whether the event
// is a relevant click type.
const clickState: ClickHandlerState = {
...baseState,
return runClickHandlerOn(
this.options.clickMark,
this.options.click,
view,
pos,
node,
nodePos,
event,
direct,
nodeWithPosition: { node, pos: nodePos },

getNode: (nodeType) => {
const type = isString(nodeType) ? schema.nodes[nodeType] : nodeType;

invariant(type, {
code: ErrorConstant.EXTENSION,
message: 'The node being checked does not exist',
});

return type === node.type ? { node, pos: nodePos } : undefined;
},
};

// Store this event so that marks aren't re-run for identical events.
eventMap.set(event, true);

return this.options.click(event, clickState) || returnValue;
);
},
handleDoubleClickOn: (view, pos, node, nodePos, event, direct) => {
return runClickHandlerOn(
this.options.doubleClickMark,
this.options.doubleClick,
view,
pos,
node,
nodePos,
event,
direct,
);
},
handleTripleClickOn: (view, pos, node, nodePos, event, direct) => {
return runClickHandlerOn(
this.options.tripleClickMark,
this.options.tripleClick,
view,
pos,
node,
nodePos,
event,
direct,
);
},

handleDOMEvents: {
Expand Down

1 comment on commit 2e19230

@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://6327f3adc2a8214b03629962--remirror.netlify.app

Please sign in to comment.