@modusoperandi/licit-floatingmenu 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FloatingMenuDefaults.d.ts +6 -0
- package/FloatingMenuDefaults.js +72 -0
- package/FloatingMenuPlugin.d.ts +24 -3
- package/FloatingMenuPlugin.js +153 -90
- package/FloatingPopup.d.ts +4 -16
- package/FloatingPopup.js +7 -34
- package/model.d.ts +11 -0
- package/package.json +7 -7
- package/slice.js +44 -14
- package/ui/menu.css +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
export function getDefaultMenuItems(handlers) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
id: 'comment',
|
|
9
|
+
label: 'Add Comment',
|
|
10
|
+
isEnabled: handlers.enableCitationAndComment,
|
|
11
|
+
onClick: handlers.addComment,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'tag',
|
|
15
|
+
label: 'Add Tag',
|
|
16
|
+
isEnabled: handlers.enableCitationAndComment,
|
|
17
|
+
onClick: handlers.addTag,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'citation',
|
|
21
|
+
label: 'Create Citation',
|
|
22
|
+
isEnabled: handlers.enableCitationAndComment,
|
|
23
|
+
onClick: handlers.createCitation,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'info',
|
|
27
|
+
label: 'Create Infoicon',
|
|
28
|
+
isEnabled: handlers.enableTagAndInfoicon,
|
|
29
|
+
onClick: handlers.createInfoIcon,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'copy',
|
|
33
|
+
label: 'Copy (Ctrl + C)',
|
|
34
|
+
isEnabled: handlers.enableCopy,
|
|
35
|
+
onClick: handlers.copyRich,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'copy-plain',
|
|
39
|
+
label: 'Copy Without Formatting',
|
|
40
|
+
isEnabled: handlers.enableCopy,
|
|
41
|
+
onClick: handlers.copyPlain,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'paste',
|
|
45
|
+
label: 'Paste (Ctrl + V)',
|
|
46
|
+
isEnabled: handlers.enablePaste,
|
|
47
|
+
onClick: handlers.paste,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'paste-plain',
|
|
51
|
+
label: 'Paste As Plain Text',
|
|
52
|
+
isEnabled: handlers.enablePaste,
|
|
53
|
+
onClick: handlers.pastePlain,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'paste-ref',
|
|
57
|
+
label: 'Paste As Reference (Ctrl + Alt + V)',
|
|
58
|
+
isEnabled: handlers.enablePasteAsReference,
|
|
59
|
+
onClick: handlers.pasteAsReference,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'slice',
|
|
63
|
+
label: 'Create Referent',
|
|
64
|
+
onClick: handlers.createSlice,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'insert-ref',
|
|
68
|
+
label: 'Insert Reference',
|
|
69
|
+
onClick: handlers.showReferences,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
package/FloatingMenuPlugin.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Node, Schema } from 'prosemirror-model';
|
|
|
8
8
|
import { Plugin, PluginKey, EditorState } from 'prosemirror-state';
|
|
9
9
|
import { PopUpHandle, Rect } from '@modusoperandi/licit-ui-commands';
|
|
10
10
|
import { createSliceManager } from './slice';
|
|
11
|
-
import { FloatRuntime } from './model';
|
|
11
|
+
import { FloatRuntime, FloatingMenuItem } from './model';
|
|
12
12
|
export declare const CMPluginKey: PluginKey<FloatingMenuPlugin>;
|
|
13
13
|
interface SliceModel {
|
|
14
14
|
name: string;
|
|
@@ -32,8 +32,9 @@ export declare class FloatingMenuPlugin extends Plugin {
|
|
|
32
32
|
_popUpHandle: PopUpHandle | null;
|
|
33
33
|
_view: EditorView | null;
|
|
34
34
|
_urlConfig: UrlConfig | null;
|
|
35
|
+
menuItems?: FloatingMenuItem[];
|
|
35
36
|
sliceManager: ReturnType<typeof createSliceManager>;
|
|
36
|
-
constructor(sliceRuntime: FloatRuntime, urlConfig?: UrlConfig);
|
|
37
|
+
constructor(sliceRuntime: FloatRuntime, urlConfig?: UrlConfig, menuItems?: FloatingMenuItem[]);
|
|
37
38
|
initKeyCommands(): Plugin[];
|
|
38
39
|
getEffectiveSchema(schema: Schema): Schema;
|
|
39
40
|
}
|
|
@@ -47,7 +48,27 @@ export declare function clipboardHasData(): Promise<boolean>;
|
|
|
47
48
|
export declare function clipboardHasProseMirrorData(): Promise<boolean>;
|
|
48
49
|
export declare function getDecorations(doc: Node, state: EditorState): DecorationSet;
|
|
49
50
|
export declare function positionAboveOrBelow(anchorRect?: Rect, bodyRect?: Rect): Rect;
|
|
50
|
-
export declare function
|
|
51
|
+
export declare function createMenuCallbacks(view: EditorView, plugin: FloatingMenuPlugin, hasClipboard?: boolean, hasPM?: boolean): {
|
|
52
|
+
enableCopy: () => boolean;
|
|
53
|
+
enablePaste: () => boolean;
|
|
54
|
+
enablePasteAsReference: () => boolean;
|
|
55
|
+
enableCitationAndComment: () => boolean;
|
|
56
|
+
enableTagAndInfoicon: () => boolean;
|
|
57
|
+
copyRich: () => void;
|
|
58
|
+
copyPlain: () => void;
|
|
59
|
+
paste: () => Promise<void>;
|
|
60
|
+
pastePlain: () => Promise<void>;
|
|
61
|
+
pasteAsReference: () => Promise<void>;
|
|
62
|
+
createCitation: () => void;
|
|
63
|
+
createInfoIcon: () => void;
|
|
64
|
+
createSlice: () => void;
|
|
65
|
+
showReferences: () => Promise<void>;
|
|
66
|
+
addComment: () => void;
|
|
67
|
+
addTag: () => void;
|
|
68
|
+
};
|
|
69
|
+
export declare function closeExistingPopup(plugin: FloatingMenuPlugin): void;
|
|
70
|
+
export declare function createOnCloseHandler(plugin: FloatingMenuPlugin, anchorEl?: HTMLElement): () => void;
|
|
71
|
+
export declare function openFloatingMenu(plugin: FloatingMenuPlugin, view: EditorView, pos?: number, anchorEl?: HTMLElement, contextPos?: {
|
|
51
72
|
x: number;
|
|
52
73
|
y: number;
|
|
53
74
|
}): void;
|
package/FloatingMenuPlugin.js
CHANGED
|
@@ -12,17 +12,50 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
12
12
|
import { insertReference } from '@modusoperandi/licit-referencing';
|
|
13
13
|
import { createSliceManager } from './slice';
|
|
14
14
|
import { createKeyMapPlugin, makeKeyMapWithCommon } from '@modusoperandi/licit-doc-attrs-step';
|
|
15
|
+
import { getDefaultMenuItems } from './FloatingMenuDefaults';
|
|
15
16
|
export const CMPluginKey = new PluginKey('floating-menu');
|
|
16
17
|
export const KEY_COPY = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-c');
|
|
17
18
|
export const KEY_CUT = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-x');
|
|
18
19
|
export const KEY_PASTE = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-v');
|
|
19
20
|
export const KEY_PASTE_REF = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-Alt-v');
|
|
21
|
+
const EMPTY_DECORATIONS = DecorationSet.empty;
|
|
22
|
+
function stepAddsParagraph(content) {
|
|
23
|
+
if (!Array.isArray(content)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return content.some((item) => {
|
|
27
|
+
if (!item || typeof item !== 'object') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const node = item;
|
|
31
|
+
return node.type === 'paragraph' || stepAddsParagraph(node.content);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function shouldRescanDecorations(tr) {
|
|
35
|
+
const forceRescan = typeof tr.getMeta === 'function'
|
|
36
|
+
? tr.getMeta(CMPluginKey)?.forceRescan
|
|
37
|
+
: false;
|
|
38
|
+
if (forceRescan) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return tr.steps.some((step) => {
|
|
42
|
+
const serializedStep = step.toJSON();
|
|
43
|
+
if (serializedStep.stepType === 'setNodeMarkup' || serializedStep.stepType === 'replaceAround') {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (serializedStep.stepType !== 'replace') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return stepAddsParagraph(serializedStep.slice?.content);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
20
52
|
export class FloatingMenuPlugin extends Plugin {
|
|
21
53
|
_popUpHandle = null;
|
|
22
54
|
_view = null;
|
|
23
55
|
_urlConfig = null;
|
|
56
|
+
menuItems;
|
|
24
57
|
sliceManager;
|
|
25
|
-
constructor(sliceRuntime, urlConfig = {}) {
|
|
58
|
+
constructor(sliceRuntime, urlConfig = {}, menuItems) {
|
|
26
59
|
const sliceManager = createSliceManager(sliceRuntime);
|
|
27
60
|
super({
|
|
28
61
|
key: CMPluginKey,
|
|
@@ -33,21 +66,19 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
33
66
|
};
|
|
34
67
|
},
|
|
35
68
|
apply(tr, prev, _oldState, newState) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
const forceRescan = typeof tr.getMeta === 'function'
|
|
70
|
+
? tr.getMeta(CMPluginKey)?.forceRescan
|
|
71
|
+
: false;
|
|
72
|
+
const mappedDecorations = prev.decorations
|
|
73
|
+
? prev.decorations.map(tr.mapping, tr.doc)
|
|
74
|
+
: EMPTY_DECORATIONS;
|
|
75
|
+
if (!tr.docChanged && !forceRescan) {
|
|
76
|
+
return { decorations: mappedDecorations };
|
|
39
77
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const s = step.toJSON();
|
|
43
|
-
return (s.stepType === 'replace' ||
|
|
44
|
-
s.stepType === 'replaceAround' ||
|
|
45
|
-
s.stepType === 'setNodeMarkup');
|
|
46
|
-
}) || tr.getMeta(CMPluginKey)?.forceRescan;
|
|
47
|
-
if (requiresRescan) {
|
|
48
|
-
decos = getDecorations(tr.doc, newState);
|
|
78
|
+
if (shouldRescanDecorations(tr)) {
|
|
79
|
+
return { decorations: getDecorations(tr.doc, newState) };
|
|
49
80
|
}
|
|
50
|
-
return { decorations:
|
|
81
|
+
return { decorations: mappedDecorations };
|
|
51
82
|
},
|
|
52
83
|
},
|
|
53
84
|
props: {
|
|
@@ -62,7 +93,7 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
62
93
|
plugin.sliceManager = sliceManager;
|
|
63
94
|
plugin._urlConfig = urlConfig;
|
|
64
95
|
getDocSlices.call(plugin, view);
|
|
65
|
-
|
|
96
|
+
const pointerDownHandler = (e) => {
|
|
66
97
|
const targetEl = getClosestHTMLElement(e.target, '.float-icon');
|
|
67
98
|
if (!targetEl)
|
|
68
99
|
return;
|
|
@@ -72,9 +103,10 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
72
103
|
wrapper?.classList.add('popup-open');
|
|
73
104
|
const pos = Number(targetEl.dataset.pos);
|
|
74
105
|
openFloatingMenu(plugin, view, pos, targetEl);
|
|
75
|
-
}
|
|
106
|
+
};
|
|
107
|
+
view.dom.addEventListener('pointerdown', pointerDownHandler);
|
|
76
108
|
// --- Alt + Right Click handler ---
|
|
77
|
-
|
|
109
|
+
const contextMenuHandler = (e) => {
|
|
78
110
|
if (e.altKey && e.button === 2 && view.editable) {
|
|
79
111
|
e.preventDefault();
|
|
80
112
|
e.stopPropagation();
|
|
@@ -84,7 +116,8 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
84
116
|
};
|
|
85
117
|
openFloatingMenu(plugin, view, undefined, undefined, pos);
|
|
86
118
|
}
|
|
87
|
-
}
|
|
119
|
+
};
|
|
120
|
+
view.dom.addEventListener('contextmenu', contextMenuHandler);
|
|
88
121
|
// --- Close popup on outside click ---
|
|
89
122
|
const outsideClickHandler = (e) => {
|
|
90
123
|
const el = e.target;
|
|
@@ -96,9 +129,18 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
96
129
|
}
|
|
97
130
|
};
|
|
98
131
|
document.addEventListener('click', outsideClickHandler);
|
|
99
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
destroy() {
|
|
134
|
+
view.dom.removeEventListener('pointerdown', pointerDownHandler);
|
|
135
|
+
view.dom.removeEventListener('contextmenu', contextMenuHandler);
|
|
136
|
+
document.removeEventListener('click', outsideClickHandler);
|
|
137
|
+
closeExistingPopup(plugin);
|
|
138
|
+
plugin._view = null;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
100
141
|
},
|
|
101
142
|
});
|
|
143
|
+
this.menuItems = menuItems;
|
|
102
144
|
}
|
|
103
145
|
initKeyCommands() {
|
|
104
146
|
return createKeyMapPlugin([
|
|
@@ -338,53 +380,58 @@ export function getDecorations(doc, state) {
|
|
|
338
380
|
(node, pos) => {
|
|
339
381
|
if (node.type.name !== 'paragraph')
|
|
340
382
|
return;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
383
|
+
decorations.push(Decoration.widget(pos + 1, () => {
|
|
384
|
+
const wrapper = document.createElement('span');
|
|
385
|
+
wrapper.className = 'pm-hamburger-wrapper';
|
|
386
|
+
const hamburger = document.createElement('span');
|
|
387
|
+
hamburger.className = 'float-icon fa fa-bars';
|
|
388
|
+
hamburger.style.fontFamily = 'FontAwesome'; // for fa compatibility
|
|
389
|
+
hamburger.dataset.pos = String(pos);
|
|
390
|
+
wrapper.appendChild(hamburger);
|
|
391
|
+
return wrapper;
|
|
392
|
+
}, {
|
|
393
|
+
key: `float-icon-${node.attrs?.objectId ?? pos}`,
|
|
394
|
+
side: 1,
|
|
395
|
+
}));
|
|
349
396
|
const decoFlags = node.attrs?.isDeco;
|
|
350
397
|
if (!decoFlags)
|
|
351
398
|
return;
|
|
352
399
|
if (decoFlags.isSlice || decoFlags.isTag || decoFlags.isComment) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
400
|
+
decorations.push(Decoration.widget(pos + 1, () => {
|
|
401
|
+
const container = document.createElement('span');
|
|
402
|
+
container.style.position = 'absolute';
|
|
403
|
+
container.style.left = '27px';
|
|
404
|
+
container.style.display = 'inline-flex';
|
|
405
|
+
container.style.gap = '6px';
|
|
406
|
+
container.style.alignItems = 'center';
|
|
407
|
+
container.contentEditable = 'false';
|
|
408
|
+
container.style.userSelect = 'none';
|
|
409
|
+
if (decoFlags.isSlice) {
|
|
410
|
+
const sliceMark = document.createElement('span');
|
|
411
|
+
sliceMark.style.fontFamily = 'FontAwesome';
|
|
412
|
+
sliceMark.innerHTML = '';
|
|
413
|
+
sliceMark.onclick = () => { };
|
|
414
|
+
container.appendChild(sliceMark);
|
|
415
|
+
}
|
|
416
|
+
if (decoFlags.isTag) {
|
|
417
|
+
const tagMark = document.createElement('span');
|
|
418
|
+
tagMark.style.fontFamily = 'FontAwesome';
|
|
419
|
+
tagMark.innerHTML = '';
|
|
420
|
+
tagMark.onclick = () => { };
|
|
421
|
+
container.appendChild(tagMark);
|
|
422
|
+
}
|
|
423
|
+
if (decoFlags.isComment) {
|
|
424
|
+
const commentMark = document.createElement('span');
|
|
425
|
+
commentMark.style.fontFamily = 'FontAwesome';
|
|
426
|
+
commentMark.innerHTML = '';
|
|
427
|
+
commentMark.onclick = () => { };
|
|
428
|
+
container.appendChild(commentMark);
|
|
429
|
+
}
|
|
430
|
+
return container;
|
|
431
|
+
}, {
|
|
432
|
+
key: `float-marks-${node.attrs?.objectId ?? pos}-${Number(!!decoFlags.isSlice)}${Number(!!decoFlags.isTag)}${Number(!!decoFlags.isComment)}`,
|
|
433
|
+
side: -1,
|
|
434
|
+
}));
|
|
388
435
|
}
|
|
389
436
|
});
|
|
390
437
|
return DecorationSet.create(state.doc, decorations);
|
|
@@ -426,46 +473,60 @@ export function positionAboveOrBelow(anchorRect, bodyRect) {
|
|
|
426
473
|
h: Math.round(menuH),
|
|
427
474
|
};
|
|
428
475
|
}
|
|
429
|
-
export function
|
|
430
|
-
|
|
476
|
+
export function createMenuCallbacks(view, plugin, hasClipboard = true, hasPM = true) {
|
|
477
|
+
return {
|
|
478
|
+
enableCopy: () => !view.state.selection.empty,
|
|
479
|
+
enablePaste: () => hasClipboard,
|
|
480
|
+
enablePasteAsReference: () => hasPM,
|
|
481
|
+
enableCitationAndComment: () => !view.state.selection.empty,
|
|
482
|
+
enableTagAndInfoicon: () => true,
|
|
483
|
+
copyRich: () => copySelectionRich(view, plugin),
|
|
484
|
+
copyPlain: () => copySelectionPlain(view, plugin),
|
|
485
|
+
paste: () => pasteFromClipboard(view, plugin),
|
|
486
|
+
pastePlain: () => pasteAsPlainText(view, plugin),
|
|
487
|
+
pasteAsReference: () => pasteAsReference(view, plugin),
|
|
488
|
+
createCitation: () => createCitationHandler(view),
|
|
489
|
+
createInfoIcon: () => createInfoIconHandler(view),
|
|
490
|
+
createSlice: () => createNewSlice(view),
|
|
491
|
+
showReferences: () => showReferences(view),
|
|
492
|
+
addComment: () => { },
|
|
493
|
+
addTag: () => { },
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
export function closeExistingPopup(plugin) {
|
|
431
497
|
if (plugin._popUpHandle) {
|
|
432
498
|
plugin._popUpHandle.close(null);
|
|
433
|
-
plugin._popUpHandle = null;
|
|
434
499
|
}
|
|
435
|
-
|
|
500
|
+
}
|
|
501
|
+
export function createOnCloseHandler(plugin, anchorEl) {
|
|
502
|
+
return () => {
|
|
503
|
+
plugin._popUpHandle = null;
|
|
504
|
+
anchorEl?.closest('.pm-hamburger-wrapper')?.classList.remove('popup-open');
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
export function openFloatingMenu(plugin, view, pos, anchorEl, contextPos) {
|
|
508
|
+
closeExistingPopup(plugin);
|
|
436
509
|
Promise.all([clipboardHasProseMirrorData(), clipboardHasData()])
|
|
437
|
-
.then(([hasPM,
|
|
438
|
-
|
|
510
|
+
.then(([hasPM, hasClipboard]) => {
|
|
511
|
+
const ctx = {
|
|
439
512
|
editorState: view.state,
|
|
440
|
-
editorView: view,
|
|
441
513
|
paragraphPos: pos,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
pastePlainHandler: () => pasteAsPlainText(view, plugin),
|
|
449
|
-
createInfoIconHandler: () => createInfoIconHandler(view),
|
|
450
|
-
createCitationHandler: () => createCitationHandler(view),
|
|
451
|
-
createNewSliceHandler: () => createNewSlice(view),
|
|
452
|
-
showReferencesHandler: () => showReferences(view),
|
|
514
|
+
};
|
|
515
|
+
const items = plugin.menuItems ??
|
|
516
|
+
getDefaultMenuItems(createMenuCallbacks(view, plugin, hasClipboard, hasPM));
|
|
517
|
+
plugin._popUpHandle = createPopUp(FloatingMenu, {
|
|
518
|
+
context: ctx,
|
|
519
|
+
items,
|
|
453
520
|
}, {
|
|
454
521
|
anchor: anchorEl || view.dom,
|
|
455
|
-
contextPos
|
|
522
|
+
contextPos,
|
|
456
523
|
position: positionAboveOrBelow,
|
|
457
524
|
autoDismiss: false,
|
|
458
|
-
onClose: ()
|
|
459
|
-
plugin._popUpHandle = null;
|
|
460
|
-
// Remove 'popup-open' class if anchor is a hamburger wrapper
|
|
461
|
-
anchorEl
|
|
462
|
-
?.closest('.pm-hamburger-wrapper')
|
|
463
|
-
?.classList.remove('popup-open');
|
|
464
|
-
},
|
|
525
|
+
onClose: createOnCloseHandler(plugin, anchorEl),
|
|
465
526
|
});
|
|
466
527
|
})
|
|
467
|
-
.catch(err => {
|
|
468
|
-
console.error('Failed to
|
|
528
|
+
.catch((err) => {
|
|
529
|
+
console.error('Failed to open floating menu:', err);
|
|
469
530
|
});
|
|
470
531
|
}
|
|
471
532
|
export function addAltRightClickHandler(view, plugin) {
|
|
@@ -496,6 +557,8 @@ export function changeAttribute(_view) {
|
|
|
496
557
|
const node = _view.state.doc.nodeAt(from);
|
|
497
558
|
if (!node)
|
|
498
559
|
return; // early return if node does not exist
|
|
560
|
+
if (node.attrs?.isDeco?.isSlice)
|
|
561
|
+
return;
|
|
499
562
|
let tr = _view.state.tr;
|
|
500
563
|
const newattrs = { ...node.attrs };
|
|
501
564
|
const isDeco = { ...newattrs.isDeco };
|
package/FloatingPopup.d.ts
CHANGED
|
@@ -3,24 +3,12 @@
|
|
|
3
3
|
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { FloatingMenuItem, FloatingMenuContext } from './model';
|
|
7
7
|
interface FloatingMenuProps {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
pasteAsReferenceEnabled: boolean;
|
|
11
|
-
enablePasteAsPlainText: boolean;
|
|
12
|
-
copyRichHandler: () => void;
|
|
13
|
-
copyPlainHandler: () => void;
|
|
14
|
-
createCitationHandler: () => void;
|
|
15
|
-
createInfoIconHandler: () => void;
|
|
16
|
-
pasteHandler: () => void;
|
|
17
|
-
pasteAsReferenceHandler: () => void;
|
|
18
|
-
pastePlainHandler: () => void;
|
|
19
|
-
createNewSliceHandler: () => void;
|
|
20
|
-
showReferencesHandler: () => void;
|
|
8
|
+
context: FloatingMenuContext;
|
|
9
|
+
items: FloatingMenuItem[];
|
|
21
10
|
}
|
|
22
|
-
export declare class FloatingMenu extends React.PureComponent<FloatingMenuProps
|
|
23
|
-
constructor(props: any);
|
|
11
|
+
export declare class FloatingMenu extends React.PureComponent<FloatingMenuProps> {
|
|
24
12
|
render(): React.ReactNode;
|
|
25
13
|
}
|
|
26
14
|
export {};
|
package/FloatingPopup.js
CHANGED
|
@@ -5,41 +5,14 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import { CustomButton } from '@modusoperandi/licit-ui-commands';
|
|
7
7
|
export class FloatingMenu extends React.PureComponent {
|
|
8
|
-
constructor(props) {
|
|
9
|
-
super(props);
|
|
10
|
-
this.state = {
|
|
11
|
-
...props,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
8
|
render() {
|
|
15
|
-
const {
|
|
16
|
-
const { selection } = editorState;
|
|
17
|
-
const $from = selection.$from;
|
|
18
|
-
const $to = selection.$to;
|
|
19
|
-
const inThisParagraph = $from.before($from.depth) === paragraphPos &&
|
|
20
|
-
$to.before($to.depth) === paragraphPos;
|
|
21
|
-
const isTextSelected = inThisParagraph && !selection.empty;
|
|
22
|
-
const enableCitationAndComment = isTextSelected;
|
|
23
|
-
const enableTagAndInfoicon = inThisParagraph;
|
|
24
|
-
const enableCopy = !selection.empty;
|
|
9
|
+
const { context, items } = this.props;
|
|
25
10
|
return (React.createElement("div", { className: "context-menu", role: "menu", tabIndex: -1 },
|
|
26
|
-
React.createElement("div", { className: "context-menu__items" },
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
React.createElement(CustomButton, {
|
|
31
|
-
|
|
32
|
-
this.props.pasteHandler();
|
|
33
|
-
} }),
|
|
34
|
-
React.createElement(CustomButton, { disabled: !this.props.enablePasteAsPlainText, label: "Paste As Plain Text", onClick: this.props.pastePlainHandler }),
|
|
35
|
-
React.createElement(CustomButton, { disabled: !this.props.pasteAsReferenceEnabled, label: "Paste As Reference(Ctrl + Alt + V)", onClick: () => {
|
|
36
|
-
this.props.pasteAsReferenceHandler();
|
|
37
|
-
} }),
|
|
38
|
-
React.createElement(CustomButton, { label: "Create Bookmark", onClick: () => {
|
|
39
|
-
this.props.createNewSliceHandler();
|
|
40
|
-
} }),
|
|
41
|
-
React.createElement(CustomButton, { label: "Insert Reference", onClick: () => {
|
|
42
|
-
this.props.showReferencesHandler();
|
|
43
|
-
} }))));
|
|
11
|
+
React.createElement("div", { className: "context-menu__items" }, items.map((item) => {
|
|
12
|
+
const enabled = item.isEnabled
|
|
13
|
+
? item.isEnabled(context)
|
|
14
|
+
: true;
|
|
15
|
+
return (React.createElement(CustomButton, { key: item.id, label: item.label, disabled: !enabled, onClick: item.onClick }));
|
|
16
|
+
}))));
|
|
44
17
|
}
|
|
45
18
|
}
|
package/model.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @license MIT
|
|
3
3
|
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
|
+
import { EditorState } from 'prosemirror-state';
|
|
5
6
|
export interface SliceModel {
|
|
6
7
|
name: string;
|
|
7
8
|
description: string;
|
|
@@ -19,3 +20,13 @@ export interface FloatRuntime {
|
|
|
19
20
|
insertCitationFloat(): void;
|
|
20
21
|
insertReference(): Promise<SliceModel>;
|
|
21
22
|
}
|
|
23
|
+
export interface FloatingMenuContext {
|
|
24
|
+
editorState: EditorState;
|
|
25
|
+
paragraphPos?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface FloatingMenuItem {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
onClick: () => void;
|
|
31
|
+
isEnabled?: (ctx: FloatingMenuContext) => boolean;
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusoperandi/licit-floatingmenu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "licit-floatingmenu plugin built with ProseMirror",
|
|
6
6
|
"main": "index.js",
|
|
@@ -41,18 +41,18 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
|
|
44
|
-
"@modusoperandi/eslint-config": "^3.0.
|
|
44
|
+
"@modusoperandi/eslint-config": "^3.0.4",
|
|
45
45
|
"@testing-library/jest-dom": "^6.9.1",
|
|
46
|
-
"@testing-library/react": "^16.3.
|
|
46
|
+
"@testing-library/react": "^16.3.2",
|
|
47
47
|
"@testing-library/user-event": "^14.6.1",
|
|
48
48
|
"@types/jest": "^29.5.14",
|
|
49
|
-
"@types/node": "^20.19.
|
|
49
|
+
"@types/node": "^20.19.37",
|
|
50
50
|
"@types/orderedmap": "^2.0.0",
|
|
51
|
-
"@types/react": "^18.3.
|
|
51
|
+
"@types/react": "^18.3.28",
|
|
52
52
|
"@types/react-dom": "^18.3.7",
|
|
53
53
|
"copyfiles": "^2.4.1",
|
|
54
54
|
"enzyme": "^3.11.0",
|
|
55
|
-
"eslint": "^9.39.
|
|
55
|
+
"eslint": "^9.39.4",
|
|
56
56
|
"husky": "^9.1.7",
|
|
57
57
|
"identity-obj-proxy": "^3.0.0",
|
|
58
58
|
"jest": "^29.7.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"jest-prosemirror": "^2.2.0",
|
|
63
63
|
"jest-sonar-reporter": "^2.0.0",
|
|
64
64
|
"lint-staged": "^15.5.2",
|
|
65
|
-
"prettier": "^3.
|
|
65
|
+
"prettier": "^3.8.1",
|
|
66
66
|
"stylelint-config-standard": "^36.0.1",
|
|
67
67
|
"stylelint-prettier": "^5.0.3",
|
|
68
68
|
"ts-jest": "^29.4.6",
|
package/slice.js
CHANGED
|
@@ -2,13 +2,33 @@
|
|
|
2
2
|
* @license MIT
|
|
3
3
|
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
|
+
function getSliceCacheKey(slice) {
|
|
6
|
+
return [
|
|
7
|
+
slice.id ?? '',
|
|
8
|
+
slice.source ?? '',
|
|
9
|
+
slice.from ?? '',
|
|
10
|
+
slice.to ?? '',
|
|
11
|
+
slice.referenceType ?? '',
|
|
12
|
+
].join('::');
|
|
13
|
+
}
|
|
14
|
+
function dedupeSlices(slices) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
return slices.filter((slice) => {
|
|
17
|
+
const key = getSliceCacheKey(slice);
|
|
18
|
+
if (seen.has(key)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
seen.add(key);
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
5
25
|
export function createSliceManager(runtime) {
|
|
6
26
|
let docSlices = [];
|
|
7
27
|
// store slices in cache
|
|
8
28
|
function setSlices(slices, state) {
|
|
9
29
|
const objectId = state.doc.attrs.objectId;
|
|
10
30
|
const filteredSlices = slices.filter((slice) => slice.source === objectId);
|
|
11
|
-
docSlices = [...docSlices, ...filteredSlices];
|
|
31
|
+
docSlices = dedupeSlices([...docSlices, ...filteredSlices]);
|
|
12
32
|
}
|
|
13
33
|
function getDocSlices() {
|
|
14
34
|
return docSlices;
|
|
@@ -19,25 +39,35 @@ export function createSliceManager(runtime) {
|
|
|
19
39
|
}
|
|
20
40
|
// add new slice to cache
|
|
21
41
|
function addSliceToList(slice) {
|
|
22
|
-
docSlices
|
|
42
|
+
docSlices = dedupeSlices([...docSlices, slice]);
|
|
23
43
|
return docSlices;
|
|
24
44
|
}
|
|
25
45
|
// apply slice attributes to the doc
|
|
26
46
|
function setSliceAttrs(view) {
|
|
27
47
|
const result = getDocSlices();
|
|
48
|
+
const sliceIds = new Set(result.map((slice) => slice.from).filter(Boolean));
|
|
49
|
+
if (sliceIds.size === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
28
52
|
let tr = view.state.tr;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
53
|
+
let hasChanges = false;
|
|
54
|
+
view.state.doc.descendants((nodeactual, pos) => {
|
|
55
|
+
if (!sliceIds.has(nodeactual?.attrs?.objectId)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (nodeactual.attrs?.isDeco?.isSlice) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const newattrs = { ...nodeactual.attrs };
|
|
62
|
+
const isDeco = { ...newattrs.isDeco };
|
|
63
|
+
isDeco.isSlice = true;
|
|
64
|
+
newattrs.isDeco = isDeco;
|
|
65
|
+
tr = tr.setNodeMarkup(pos, undefined, newattrs);
|
|
66
|
+
hasChanges = true;
|
|
67
|
+
});
|
|
68
|
+
if (hasChanges) {
|
|
69
|
+
view.dispatch(tr);
|
|
39
70
|
}
|
|
40
|
-
view.dispatch(tr);
|
|
41
71
|
}
|
|
42
72
|
function addInfoIcon() {
|
|
43
73
|
return runtime?.insertInfoIconFloat();
|
|
@@ -60,6 +90,6 @@ export function createSliceManager(runtime) {
|
|
|
60
90
|
addInfoIcon,
|
|
61
91
|
addCitation,
|
|
62
92
|
createSliceViaDialog,
|
|
63
|
-
insertReference
|
|
93
|
+
insertReference,
|
|
64
94
|
};
|
|
65
95
|
}
|