@modusoperandi/licit-floatingmenu 0.1.0 → 0.1.1
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/FloatingMenuPlugin.d.ts +20 -2
- package/FloatingMenuPlugin.js +140 -42
- package/FloatingPopup.d.ts +7 -4
- package/FloatingPopup.js +15 -10
- package/LICENSE +21 -21
- package/index.d.ts +4 -0
- package/index.js +4 -0
- package/model.d.ts +4 -0
- package/model.js +4 -0
- package/package.json +28 -29
- package/slice.d.ts +4 -0
- package/slice.js +7 -3
- package/ui/menu.css +1 -2
package/FloatingMenuPlugin.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
* @file A generic Floating Menu ProseMirror Plugin
|
|
5
|
+
*/
|
|
1
6
|
import { DecorationSet, EditorView } from 'prosemirror-view';
|
|
2
7
|
import { Node, Schema } from 'prosemirror-model';
|
|
3
8
|
import { Plugin, PluginKey, EditorState } from 'prosemirror-state';
|
|
4
|
-
import { PopUpHandle } from '@modusoperandi/licit-ui-commands';
|
|
9
|
+
import { PopUpHandle, Rect } from '@modusoperandi/licit-ui-commands';
|
|
5
10
|
import { createSliceManager } from './slice';
|
|
6
11
|
import { FloatRuntime } from './model';
|
|
7
12
|
export declare const CMPluginKey: PluginKey<FloatingMenuPlugin>;
|
|
@@ -15,11 +20,21 @@ interface SliceModel {
|
|
|
15
20
|
to: string;
|
|
16
21
|
ids: string[];
|
|
17
22
|
}
|
|
23
|
+
export declare const KEY_COPY: any;
|
|
24
|
+
export declare const KEY_CUT: any;
|
|
25
|
+
export declare const KEY_PASTE: any;
|
|
26
|
+
export declare const KEY_PASTE_REF: any;
|
|
27
|
+
interface UrlConfig {
|
|
28
|
+
instanceUrl?: string;
|
|
29
|
+
referenceUrl?: string;
|
|
30
|
+
}
|
|
18
31
|
export declare class FloatingMenuPlugin extends Plugin {
|
|
19
32
|
_popUpHandle: PopUpHandle | null;
|
|
20
33
|
_view: EditorView | null;
|
|
34
|
+
_urlConfig: UrlConfig | null;
|
|
21
35
|
sliceManager: ReturnType<typeof createSliceManager>;
|
|
22
|
-
constructor(sliceRuntime: FloatRuntime);
|
|
36
|
+
constructor(sliceRuntime: FloatRuntime, urlConfig?: UrlConfig);
|
|
37
|
+
initKeyCommands(): Plugin[];
|
|
23
38
|
getEffectiveSchema(schema: Schema): Schema;
|
|
24
39
|
}
|
|
25
40
|
export declare function copySelectionRich(view: EditorView, plugin: FloatingMenuPlugin): void;
|
|
@@ -28,8 +43,10 @@ export declare function copySelectionPlain(view: EditorView, plugin: FloatingMen
|
|
|
28
43
|
export declare function pasteFromClipboard(view: EditorView, plugin: FloatingMenuPlugin): Promise<void>;
|
|
29
44
|
export declare function pasteAsReference(view: EditorView, plugin: FloatingMenuPlugin): Promise<void>;
|
|
30
45
|
export declare function pasteAsPlainText(view: EditorView, plugin: FloatingMenuPlugin): Promise<void>;
|
|
46
|
+
export declare function clipboardHasData(): Promise<boolean>;
|
|
31
47
|
export declare function clipboardHasProseMirrorData(): Promise<boolean>;
|
|
32
48
|
export declare function getDecorations(doc: Node, state: EditorState): DecorationSet;
|
|
49
|
+
export declare function positionAboveOrBelow(anchorRect?: Rect, bodyRect?: Rect): Rect;
|
|
33
50
|
export declare function openFloatingMenu(plugin: FloatingMenuPlugin, view: EditorView, pos: number, anchorEl?: HTMLElement, contextPos?: {
|
|
34
51
|
x: number;
|
|
35
52
|
y: number;
|
|
@@ -41,4 +58,5 @@ export declare function createNewSlice(view: EditorView): void;
|
|
|
41
58
|
export declare function showReferences(view: EditorView): Promise<void>;
|
|
42
59
|
export declare function createInfoIconHandler(view: EditorView): void;
|
|
43
60
|
export declare function createCitationHandler(view: EditorView): void;
|
|
61
|
+
export declare function getClosestHTMLElement(el: EventTarget | null, selector: string): HTMLElement | null;
|
|
44
62
|
export {};
|
package/FloatingMenuPlugin.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
* @file A generic Floating Menu ProseMirror Plugin
|
|
5
|
+
*/
|
|
2
6
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
3
7
|
import { Slice } from 'prosemirror-model';
|
|
4
8
|
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
5
|
-
import { createPopUp
|
|
9
|
+
import { createPopUp } from '@modusoperandi/licit-ui-commands';
|
|
6
10
|
import { FloatingMenu } from './FloatingPopup';
|
|
7
11
|
import { v4 as uuidv4 } from 'uuid';
|
|
8
12
|
import { insertReference } from '@modusoperandi/licit-referencing';
|
|
9
13
|
import { createSliceManager } from './slice';
|
|
14
|
+
import { createKeyMapPlugin, makeKeyMapWithCommon } from '@modusoperandi/licit-doc-attrs-step';
|
|
10
15
|
export const CMPluginKey = new PluginKey('floating-menu');
|
|
16
|
+
export const KEY_COPY = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-c');
|
|
17
|
+
export const KEY_CUT = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-x');
|
|
18
|
+
export const KEY_PASTE = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-v');
|
|
19
|
+
export const KEY_PASTE_REF = makeKeyMapWithCommon('FloatingMenuPlugin', 'Mod-Alt-v');
|
|
11
20
|
export class FloatingMenuPlugin extends Plugin {
|
|
12
21
|
_popUpHandle = null;
|
|
13
22
|
_view = null;
|
|
23
|
+
_urlConfig = null;
|
|
14
24
|
sliceManager;
|
|
15
|
-
constructor(sliceRuntime) {
|
|
25
|
+
constructor(sliceRuntime, urlConfig = {}) {
|
|
16
26
|
const sliceManager = createSliceManager(sliceRuntime);
|
|
17
27
|
super({
|
|
18
28
|
key: CMPluginKey,
|
|
@@ -25,9 +35,9 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
25
35
|
apply(tr, prev, _oldState, newState) {
|
|
26
36
|
let decos = prev.decorations;
|
|
27
37
|
if (!tr.docChanged) {
|
|
28
|
-
return { decorations: decos
|
|
38
|
+
return { decorations: decos ? DecorationSet.prototype.map.call(decos, tr.mapping, tr.doc) : decos };
|
|
29
39
|
}
|
|
30
|
-
decos =
|
|
40
|
+
decos = DecorationSet.prototype.map.call(decos, tr.mapping, tr.doc);
|
|
31
41
|
const requiresRescan = tr.steps.some((step) => {
|
|
32
42
|
const s = step.toJSON();
|
|
33
43
|
return (s.stepType === 'replace' ||
|
|
@@ -42,21 +52,23 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
42
52
|
},
|
|
43
53
|
props: {
|
|
44
54
|
decorations(state) {
|
|
45
|
-
|
|
55
|
+
const pluginState = this.getState(state);
|
|
56
|
+
return pluginState?.decorations;
|
|
46
57
|
},
|
|
47
58
|
},
|
|
48
59
|
view: (view) => {
|
|
49
60
|
const plugin = this;
|
|
50
61
|
plugin._view = view;
|
|
51
62
|
plugin.sliceManager = sliceManager;
|
|
63
|
+
plugin._urlConfig = urlConfig;
|
|
52
64
|
getDocSlices.call(plugin, view);
|
|
53
65
|
view.dom.addEventListener('pointerdown', (e) => {
|
|
54
|
-
const targetEl = e.target
|
|
66
|
+
const targetEl = getClosestHTMLElement(e.target, '.float-icon');
|
|
55
67
|
if (!targetEl)
|
|
56
68
|
return;
|
|
57
69
|
e.preventDefault();
|
|
58
70
|
e.stopPropagation();
|
|
59
|
-
const wrapper = targetEl
|
|
71
|
+
const wrapper = getClosestHTMLElement(targetEl, '.pm-hamburger-wrapper');
|
|
60
72
|
wrapper?.classList.add('popup-open');
|
|
61
73
|
const pos = Number(targetEl.dataset.pos);
|
|
62
74
|
openFloatingMenu(plugin, view, pos, targetEl);
|
|
@@ -88,6 +100,34 @@ export class FloatingMenuPlugin extends Plugin {
|
|
|
88
100
|
},
|
|
89
101
|
});
|
|
90
102
|
}
|
|
103
|
+
initKeyCommands() {
|
|
104
|
+
return createKeyMapPlugin([
|
|
105
|
+
{
|
|
106
|
+
map: {
|
|
107
|
+
[KEY_COPY.common]: (_state, _dispatch, view) => copySelectionRich(view, this),
|
|
108
|
+
},
|
|
109
|
+
name: 'CopySlicePluginKeyCommands',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
map: {
|
|
113
|
+
[KEY_CUT.common]: (_state, _dispatch, view) => copySelectionRich(view, this),
|
|
114
|
+
},
|
|
115
|
+
name: 'CutSlicePluginKeyCommands',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
map: {
|
|
119
|
+
[KEY_PASTE.common]: (_state, _dispatch, view) => pasteFromClipboard(view, this),
|
|
120
|
+
},
|
|
121
|
+
name: 'PasteSlicePluginKeyCommands',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
map: {
|
|
125
|
+
[KEY_PASTE_REF.common]: (_state, _dispatch, view) => pasteAsReference(view, this),
|
|
126
|
+
},
|
|
127
|
+
name: 'PasteReferencePluginKeyCommands',
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
91
131
|
getEffectiveSchema(schema) {
|
|
92
132
|
return schema;
|
|
93
133
|
}
|
|
@@ -107,7 +147,7 @@ export function copySelectionRich(view, plugin) {
|
|
|
107
147
|
};
|
|
108
148
|
navigator.clipboard
|
|
109
149
|
.writeText(JSON.stringify(sliceJSON))
|
|
110
|
-
.then(() =>
|
|
150
|
+
.then(() => { })
|
|
111
151
|
.catch((err) => console.error('Clipboard write failed', err));
|
|
112
152
|
if (plugin._popUpHandle) {
|
|
113
153
|
plugin._popUpHandle.update({
|
|
@@ -121,8 +161,9 @@ export function copySelectionRich(view, plugin) {
|
|
|
121
161
|
}
|
|
122
162
|
}
|
|
123
163
|
export function createSliceObject(editorView) {
|
|
124
|
-
const
|
|
125
|
-
const referenceUrl =
|
|
164
|
+
const plugin = CMPluginKey.get(editorView.state);
|
|
165
|
+
const referenceUrl = plugin?._urlConfig?.referenceUrl;
|
|
166
|
+
const instanceUrl = plugin?._urlConfig?.instanceUrl;
|
|
126
167
|
const sliceModel = {
|
|
127
168
|
name: '',
|
|
128
169
|
description: '',
|
|
@@ -133,27 +174,30 @@ export function createSliceObject(editorView) {
|
|
|
133
174
|
to: '',
|
|
134
175
|
ids: [],
|
|
135
176
|
};
|
|
136
|
-
const objectIds = [];
|
|
137
|
-
let firstParagraphText = null;
|
|
138
177
|
editorView.focus();
|
|
139
178
|
const $from = editorView.state.selection.$from;
|
|
140
179
|
const $to = editorView.state.selection.$to;
|
|
141
180
|
const from = $from.start($from.depth);
|
|
142
181
|
const to = $to.end($to.depth);
|
|
143
|
-
|
|
182
|
+
const paragraphEntries = [];
|
|
183
|
+
editorView.state.doc.nodesBetween(from, to, (node, pos) => {
|
|
144
184
|
if (node.type.name === 'paragraph') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
185
|
+
paragraphEntries.push({
|
|
186
|
+
pos,
|
|
187
|
+
id: node.attrs?.objectId,
|
|
188
|
+
text: node.textContent?.trim() || undefined,
|
|
189
|
+
});
|
|
151
190
|
}
|
|
152
191
|
});
|
|
192
|
+
paragraphEntries.sort((a, b) => a.pos - b.pos);
|
|
193
|
+
const objectIds = paragraphEntries
|
|
194
|
+
.filter(entry => entry.id !== undefined)
|
|
195
|
+
.map(entry => entry.id);
|
|
196
|
+
const firstParagraphText = paragraphEntries.find(entry => entry.text)?.text ?? '';
|
|
153
197
|
sliceModel.id = instanceUrl + uuidv4();
|
|
154
198
|
sliceModel.ids = objectIds;
|
|
155
199
|
sliceModel.from = objectIds.length > 0 ? objectIds[0] : '';
|
|
156
|
-
sliceModel.to = objectIds.length > 0 ? objectIds
|
|
200
|
+
sliceModel.to = objectIds.length > 0 ? objectIds.at(-1) : '';
|
|
157
201
|
const viewWithDocView = editorView;
|
|
158
202
|
sliceModel.source = viewWithDocView?.['docView']?.node?.attrs?.objectId;
|
|
159
203
|
sliceModel.referenceType = referenceUrl;
|
|
@@ -173,7 +217,7 @@ export function copySelectionPlain(view, plugin) {
|
|
|
173
217
|
const text = slice.content.textBetween(0, slice.content.size, '\n');
|
|
174
218
|
navigator.clipboard
|
|
175
219
|
.writeText(text)
|
|
176
|
-
.then(() =>
|
|
220
|
+
.then(() => { })
|
|
177
221
|
.catch((err) => console.error('Clipboard write failed:', err));
|
|
178
222
|
if (plugin._popUpHandle?.close) {
|
|
179
223
|
plugin._popUpHandle.close(null);
|
|
@@ -186,14 +230,12 @@ export async function pasteFromClipboard(view, plugin) {
|
|
|
186
230
|
view.focus();
|
|
187
231
|
const text = await navigator.clipboard.readText();
|
|
188
232
|
let tr;
|
|
189
|
-
|
|
190
|
-
// Try parsing as JSON slice
|
|
233
|
+
if (text.trim().startsWith('{') || text.trim().startsWith('[')) {
|
|
191
234
|
const parsed = JSON.parse(text);
|
|
192
235
|
const slice = Slice.fromJSON(view.state.schema, parsed);
|
|
193
236
|
tr = view.state.tr.replaceSelection(slice);
|
|
194
237
|
}
|
|
195
|
-
|
|
196
|
-
// If not JSON, treat as plain text
|
|
238
|
+
else {
|
|
197
239
|
tr = view.state.tr.insertText(text, view.state.selection.from, view.state.selection.to);
|
|
198
240
|
}
|
|
199
241
|
view.dispatch(tr.scrollIntoView());
|
|
@@ -220,11 +262,9 @@ export async function pasteAsReference(view, plugin) {
|
|
|
220
262
|
}
|
|
221
263
|
const val = await plugin.sliceManager.createSliceViaDialog(sliceModel);
|
|
222
264
|
if (!val) {
|
|
223
|
-
console.warn('Slice creation returned no value, skipping insertReference.');
|
|
224
265
|
return;
|
|
225
266
|
}
|
|
226
|
-
insertReference(view, val.id, val.source, view['docView']?.node?.attrs?.objectMetaData?.name);
|
|
227
|
-
console.log('Slice created successfully:', val);
|
|
267
|
+
insertReference(view, val.id, val.source, view['docView']?.node?.attrs?.objectMetaData?.name, val.from);
|
|
228
268
|
}
|
|
229
269
|
catch (err) {
|
|
230
270
|
console.error('Failed to paste content or create slice:', err);
|
|
@@ -267,17 +307,25 @@ export async function pasteAsPlainText(view, plugin) {
|
|
|
267
307
|
plugin._popUpHandle = null;
|
|
268
308
|
}
|
|
269
309
|
}
|
|
310
|
+
export async function clipboardHasData() {
|
|
311
|
+
try {
|
|
312
|
+
const text = await navigator.clipboard.readText();
|
|
313
|
+
return !!text;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
270
319
|
export async function clipboardHasProseMirrorData() {
|
|
271
320
|
try {
|
|
272
321
|
const text = await navigator.clipboard.readText();
|
|
273
322
|
if (!text)
|
|
274
323
|
return false;
|
|
275
324
|
const parsed = JSON.parse(text);
|
|
276
|
-
return (
|
|
325
|
+
return !!(parsed &&
|
|
277
326
|
typeof parsed === 'object' &&
|
|
278
327
|
parsed.content &&
|
|
279
|
-
Array.isArray(parsed.content)
|
|
280
|
-
parsed.content.type);
|
|
328
|
+
(Array.isArray(parsed.content) || parsed.content.type));
|
|
281
329
|
}
|
|
282
330
|
catch {
|
|
283
331
|
return false;
|
|
@@ -286,13 +334,13 @@ export async function clipboardHasProseMirrorData() {
|
|
|
286
334
|
// --- Decoration function ---
|
|
287
335
|
export function getDecorations(doc, state) {
|
|
288
336
|
const decorations = [];
|
|
289
|
-
doc?.forEach(
|
|
337
|
+
doc?.forEach(// NOSONAR not an iterable
|
|
338
|
+
(node, pos) => {
|
|
290
339
|
if (node.type.name !== 'paragraph')
|
|
291
340
|
return;
|
|
292
341
|
const wrapper = document.createElement('span');
|
|
293
342
|
wrapper.className = 'pm-hamburger-wrapper';
|
|
294
343
|
const hamburger = document.createElement('span');
|
|
295
|
-
// ✅ Use FontAwesome
|
|
296
344
|
hamburger.className = 'float-icon fa fa-bars';
|
|
297
345
|
hamburger.style.fontFamily = 'FontAwesome'; // for fa compatibility
|
|
298
346
|
hamburger.dataset.pos = String(pos);
|
|
@@ -317,7 +365,7 @@ export function getDecorations(doc, state) {
|
|
|
317
365
|
SliceMark.id = `slicemark-${uuidv4()}`;
|
|
318
366
|
SliceMark.style.fontFamily = 'FontAwesome';
|
|
319
367
|
SliceMark.innerHTML = '';
|
|
320
|
-
SliceMark.onclick = () =>
|
|
368
|
+
SliceMark.onclick = () => { };
|
|
321
369
|
container.appendChild(SliceMark);
|
|
322
370
|
}
|
|
323
371
|
// --- Tag ---
|
|
@@ -325,7 +373,7 @@ export function getDecorations(doc, state) {
|
|
|
325
373
|
const TagMark = document.createElement('span');
|
|
326
374
|
TagMark.style.fontFamily = 'FontAwesome';
|
|
327
375
|
TagMark.innerHTML = '';
|
|
328
|
-
TagMark.onclick = () =>
|
|
376
|
+
TagMark.onclick = () => { };
|
|
329
377
|
container.appendChild(TagMark);
|
|
330
378
|
}
|
|
331
379
|
// --- Comment ---
|
|
@@ -333,7 +381,7 @@ export function getDecorations(doc, state) {
|
|
|
333
381
|
const CommentMark = document.createElement('span');
|
|
334
382
|
CommentMark.style.fontFamily = 'FontAwesome';
|
|
335
383
|
CommentMark.innerHTML = '';
|
|
336
|
-
CommentMark.onclick = () =>
|
|
384
|
+
CommentMark.onclick = () => { };
|
|
337
385
|
container.appendChild(CommentMark);
|
|
338
386
|
}
|
|
339
387
|
decorations.push(Decoration.widget(pos + 1, container, { side: -1 }));
|
|
@@ -341,6 +389,43 @@ export function getDecorations(doc, state) {
|
|
|
341
389
|
});
|
|
342
390
|
return DecorationSet.create(state.doc, decorations);
|
|
343
391
|
}
|
|
392
|
+
export function positionAboveOrBelow(anchorRect, bodyRect) {
|
|
393
|
+
if (!anchorRect) {
|
|
394
|
+
return { x: 4, y: 4, w: 0, h: 0 };
|
|
395
|
+
}
|
|
396
|
+
const estimatedWidth = bodyRect && bodyRect.w > 0 ? bodyRect.w : 180;
|
|
397
|
+
const estimatedHeight = bodyRect && bodyRect.h > 0 ? bodyRect.h : 220;
|
|
398
|
+
const menuW = estimatedWidth;
|
|
399
|
+
const menuH = estimatedHeight;
|
|
400
|
+
// available space below/above relative to viewport
|
|
401
|
+
const anchorBottom = anchorRect.y + anchorRect.h;
|
|
402
|
+
const spaceBelow = window.innerHeight - anchorBottom;
|
|
403
|
+
const spaceAbove = anchorRect.y;
|
|
404
|
+
let y;
|
|
405
|
+
let x;
|
|
406
|
+
if (spaceBelow < menuH && spaceAbove > menuH) {
|
|
407
|
+
y = anchorRect.y - menuH - 6; // small gap
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
y = anchorBottom + 6;
|
|
411
|
+
}
|
|
412
|
+
x = anchorRect.x;
|
|
413
|
+
if (x + menuW > window.innerWidth - 6) {
|
|
414
|
+
x = Math.max(6, window.innerWidth - menuW - 6);
|
|
415
|
+
}
|
|
416
|
+
x = Math.max(6, x);
|
|
417
|
+
if (y < 6)
|
|
418
|
+
y = 6;
|
|
419
|
+
if (y + menuH > window.innerHeight - 6) {
|
|
420
|
+
y = Math.max(6, window.innerHeight - menuH - 6);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
x: Math.round(x),
|
|
424
|
+
y: Math.round(y),
|
|
425
|
+
w: Math.round(menuW),
|
|
426
|
+
h: Math.round(menuH),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
344
429
|
export function openFloatingMenu(plugin, view, pos, anchorEl, contextPos) {
|
|
345
430
|
// Close existing popup if any
|
|
346
431
|
if (plugin._popUpHandle) {
|
|
@@ -348,12 +433,14 @@ export function openFloatingMenu(plugin, view, pos, anchorEl, contextPos) {
|
|
|
348
433
|
plugin._popUpHandle = null;
|
|
349
434
|
}
|
|
350
435
|
// Determine if clipboard has ProseMirror data
|
|
351
|
-
clipboardHasProseMirrorData()
|
|
436
|
+
Promise.all([clipboardHasProseMirrorData(), clipboardHasData()])
|
|
437
|
+
.then(([hasPM, clipboardHasData]) => {
|
|
352
438
|
plugin._popUpHandle = createPopUp(FloatingMenu, {
|
|
353
439
|
editorState: view.state,
|
|
354
440
|
editorView: view,
|
|
355
441
|
paragraphPos: pos,
|
|
356
442
|
pasteAsReferenceEnabled: hasPM,
|
|
443
|
+
enablePasteAsPlainText: clipboardHasData,
|
|
357
444
|
copyRichHandler: () => copySelectionRich(view, plugin),
|
|
358
445
|
copyPlainHandler: () => copySelectionPlain(view, plugin),
|
|
359
446
|
pasteHandler: () => pasteFromClipboard(view, plugin),
|
|
@@ -361,10 +448,12 @@ export function openFloatingMenu(plugin, view, pos, anchorEl, contextPos) {
|
|
|
361
448
|
pastePlainHandler: () => pasteAsPlainText(view, plugin),
|
|
362
449
|
createInfoIconHandler: () => createInfoIconHandler(view),
|
|
363
450
|
createCitationHandler: () => createCitationHandler(view),
|
|
451
|
+
createNewSliceHandler: () => createNewSlice(view),
|
|
452
|
+
showReferencesHandler: () => showReferences(view),
|
|
364
453
|
}, {
|
|
365
454
|
anchor: anchorEl || view.dom,
|
|
366
455
|
contextPos: contextPos,
|
|
367
|
-
position:
|
|
456
|
+
position: positionAboveOrBelow,
|
|
368
457
|
autoDismiss: false,
|
|
369
458
|
onClose: () => {
|
|
370
459
|
plugin._popUpHandle = null;
|
|
@@ -374,6 +463,9 @@ export function openFloatingMenu(plugin, view, pos, anchorEl, contextPos) {
|
|
|
374
463
|
?.classList.remove('popup-open');
|
|
375
464
|
},
|
|
376
465
|
});
|
|
466
|
+
})
|
|
467
|
+
.catch(err => {
|
|
468
|
+
console.error('Failed to check clipboard data:', err);
|
|
377
469
|
});
|
|
378
470
|
}
|
|
379
471
|
export function addAltRightClickHandler(view, plugin) {
|
|
@@ -406,7 +498,7 @@ export function changeAttribute(_view) {
|
|
|
406
498
|
return; // early return if node does not exist
|
|
407
499
|
let tr = _view.state.tr;
|
|
408
500
|
const newattrs = { ...node.attrs };
|
|
409
|
-
const isDeco = { ...
|
|
501
|
+
const isDeco = { ...newattrs.isDeco };
|
|
410
502
|
isDeco.isSlice = true;
|
|
411
503
|
newattrs.isDeco = isDeco;
|
|
412
504
|
tr = tr.setNodeMarkup(from, undefined, newattrs);
|
|
@@ -427,14 +519,14 @@ export function createNewSlice(view) {
|
|
|
427
519
|
console.error('createSlice failed with:', err);
|
|
428
520
|
});
|
|
429
521
|
}
|
|
430
|
-
export
|
|
522
|
+
export function showReferences(view) {
|
|
431
523
|
const plugin = CMPluginKey.get(view.state);
|
|
432
524
|
if (!plugin)
|
|
433
525
|
return;
|
|
434
526
|
plugin.sliceManager
|
|
435
527
|
.insertReference()
|
|
436
528
|
.then((val) => {
|
|
437
|
-
insertReference(view, val.id, val.source, view['docView']?.node?.attrs?.objectMetaData?.name);
|
|
529
|
+
insertReference(view, val.id, val.source, view['docView']?.node?.attrs?.objectMetaData?.name, val.from);
|
|
438
530
|
})
|
|
439
531
|
.catch((err) => {
|
|
440
532
|
console.error('createSlice failed with:', err);
|
|
@@ -452,3 +544,9 @@ export function createCitationHandler(view) {
|
|
|
452
544
|
return;
|
|
453
545
|
plugin.sliceManager?.addCitation();
|
|
454
546
|
}
|
|
547
|
+
export function getClosestHTMLElement(el, selector) {
|
|
548
|
+
if (!(el instanceof Element))
|
|
549
|
+
return null;
|
|
550
|
+
const closest = el.closest(selector);
|
|
551
|
+
return closest instanceof HTMLElement ? closest : null;
|
|
552
|
+
}
|
package/FloatingPopup.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
1
5
|
import React from 'react';
|
|
2
|
-
import { EditorView } from 'prosemirror-view';
|
|
3
6
|
import { EditorState } from 'prosemirror-state';
|
|
4
7
|
interface FloatingMenuProps {
|
|
5
8
|
editorState: EditorState;
|
|
6
|
-
editorView: EditorView;
|
|
7
9
|
paragraphPos: number;
|
|
8
10
|
pasteAsReferenceEnabled: boolean;
|
|
11
|
+
enablePasteAsPlainText: boolean;
|
|
9
12
|
copyRichHandler: () => void;
|
|
10
13
|
copyPlainHandler: () => void;
|
|
11
14
|
createCitationHandler: () => void;
|
|
@@ -13,11 +16,11 @@ interface FloatingMenuProps {
|
|
|
13
16
|
pasteHandler: () => void;
|
|
14
17
|
pasteAsReferenceHandler: () => void;
|
|
15
18
|
pastePlainHandler: () => void;
|
|
16
|
-
|
|
19
|
+
createNewSliceHandler: () => void;
|
|
20
|
+
showReferencesHandler: () => void;
|
|
17
21
|
}
|
|
18
22
|
export declare class FloatingMenu extends React.PureComponent<FloatingMenuProps, FloatingMenuProps> {
|
|
19
23
|
constructor(props: any);
|
|
20
24
|
render(): React.ReactNode;
|
|
21
|
-
closePopup(menuName: string): void;
|
|
22
25
|
}
|
|
23
26
|
export {};
|
package/FloatingPopup.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
1
5
|
import React from 'react';
|
|
2
6
|
import { CustomButton } from '@modusoperandi/licit-ui-commands';
|
|
3
|
-
import { createNewSlice, showReferences } from './FloatingMenuPlugin';
|
|
4
7
|
export class FloatingMenu extends React.PureComponent {
|
|
5
8
|
constructor(props) {
|
|
6
9
|
super(props);
|
|
@@ -18,23 +21,25 @@ export class FloatingMenu extends React.PureComponent {
|
|
|
18
21
|
const isTextSelected = inThisParagraph && !selection.empty;
|
|
19
22
|
const enableCitationAndComment = isTextSelected;
|
|
20
23
|
const enableTagAndInfoicon = inThisParagraph;
|
|
24
|
+
const enableCopy = !selection.empty;
|
|
21
25
|
return (React.createElement("div", { className: "context-menu", role: "menu", tabIndex: -1 },
|
|
22
26
|
React.createElement("div", { className: "context-menu__items" },
|
|
23
27
|
React.createElement(CustomButton, { disabled: !enableCitationAndComment, label: "Create Citation", onClick: this.props.createCitationHandler }),
|
|
24
28
|
React.createElement(CustomButton, { disabled: !enableTagAndInfoicon, label: "Create Infoicon", onClick: this.props.createInfoIconHandler }),
|
|
25
|
-
React.createElement(CustomButton, { label: "Copy", onClick: this.props.copyRichHandler }),
|
|
26
|
-
React.createElement(CustomButton, { label: "Copy Without Formatting", onClick: this.props.copyPlainHandler }),
|
|
27
|
-
React.createElement(CustomButton, { label: "Paste", onClick: () => {
|
|
29
|
+
React.createElement(CustomButton, { disabled: !enableCopy, label: "Copy", onClick: this.props.copyRichHandler }),
|
|
30
|
+
React.createElement(CustomButton, { disabled: !enableCopy, label: "Copy Without Formatting", onClick: this.props.copyPlainHandler }),
|
|
31
|
+
React.createElement(CustomButton, { disabled: !this.props.enablePasteAsPlainText, label: "Paste", onClick: () => {
|
|
28
32
|
this.props.pasteHandler();
|
|
29
33
|
} }),
|
|
30
|
-
React.createElement(CustomButton, { label: "Paste As Plain Text", onClick: this.props.pastePlainHandler }),
|
|
34
|
+
React.createElement(CustomButton, { disabled: !this.props.enablePasteAsPlainText, label: "Paste As Plain Text", onClick: this.props.pastePlainHandler }),
|
|
31
35
|
React.createElement(CustomButton, { disabled: !this.props.pasteAsReferenceEnabled, label: "Paste As Reference", onClick: () => {
|
|
32
36
|
this.props.pasteAsReferenceHandler();
|
|
33
37
|
} }),
|
|
34
|
-
React.createElement(CustomButton, { label: "Create Bookmark", onClick: () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
} }))));
|
|
39
44
|
}
|
|
40
45
|
}
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Modus Operandi Inc.
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Modus Operandi Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/model.d.ts
CHANGED
package/model.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusoperandi/licit-floatingmenu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "licit-floatingmenu plugin built with ProseMirror",
|
|
6
6
|
"main": "index.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"verify": "npm run lint -- --fix && npm run ci:build && npm run test:coverage && echo 'All Tests Passed!'"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
+
"@modusoperandi/licit-doc-attrs-step": "^1.0.3",
|
|
25
26
|
"@modusoperandi/licit-ui-commands": "^1.0.0",
|
|
26
27
|
"@modusoperandi/licit-referencing": "^1.0.0",
|
|
27
28
|
"prosemirror-model": "^1.19.4",
|
|
@@ -30,45 +31,43 @@
|
|
|
30
31
|
"prosemirror-view": "^1.27.0",
|
|
31
32
|
"prosemirror-schema-basic": "^1.2.0",
|
|
32
33
|
"prosemirror-schema-list": "^1.2.0",
|
|
33
|
-
"prosemirror-utils": "^1.0.0"
|
|
34
|
+
"prosemirror-utils": "^1.0.0",
|
|
35
|
+
"react": "^18.0.0"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"font-awesome": "^4.7.0"
|
|
38
|
+
"font-awesome": "^4.7.0",
|
|
39
|
+
"react-dom": "^18.3.1",
|
|
40
|
+
"uuid": "^13.0.0"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
|
|
40
|
-
"@
|
|
41
|
-
"@testing-library/
|
|
42
|
-
"@testing-library/
|
|
43
|
-
"@
|
|
44
|
-
"@types/
|
|
44
|
+
"@modusoperandi/eslint-config": "^3.0.3",
|
|
45
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
46
|
+
"@testing-library/react": "^16.3.1",
|
|
47
|
+
"@testing-library/user-event": "^14.6.1",
|
|
48
|
+
"@types/jest": "^29.5.14",
|
|
49
|
+
"@types/node": "^20.19.27",
|
|
45
50
|
"@types/orderedmap": "^2.0.0",
|
|
46
|
-
"@types/react": "^18.
|
|
47
|
-
"@types/react-dom": "^18.
|
|
48
|
-
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
49
|
-
"@typescript-eslint/parser": "^7.0.2",
|
|
51
|
+
"@types/react": "^18.3.27",
|
|
52
|
+
"@types/react-dom": "^18.3.7",
|
|
50
53
|
"copyfiles": "^2.4.1",
|
|
51
54
|
"enzyme": "^3.11.0",
|
|
52
|
-
"eslint": "^
|
|
53
|
-
"
|
|
54
|
-
"eslint-plugin-jest": "^28.2.0",
|
|
55
|
-
"eslint-plugin-prettier": "^5.1.3",
|
|
56
|
-
"eslint-plugin-react": "^7.24.0",
|
|
57
|
-
"husky": "^9.0.10",
|
|
55
|
+
"eslint": "^9.39.2",
|
|
56
|
+
"husky": "^9.1.7",
|
|
58
57
|
"identity-obj-proxy": "^3.0.0",
|
|
59
|
-
"jest": "^29.
|
|
58
|
+
"jest": "^29.7.0",
|
|
60
59
|
"jest-environment-jsdom": "^29.7.0",
|
|
61
60
|
"jest-junit": "^16.0.0",
|
|
62
|
-
"jest-mock-extended": "^3.0.
|
|
63
|
-
"jest-prosemirror": "^2.
|
|
61
|
+
"jest-mock-extended": "^3.0.7",
|
|
62
|
+
"jest-prosemirror": "^2.2.0",
|
|
64
63
|
"jest-sonar-reporter": "^2.0.0",
|
|
65
|
-
"lint-staged": "^15.2
|
|
66
|
-
"prettier": "^3.
|
|
67
|
-
"stylelint-config-standard": "^36.0.
|
|
68
|
-
"stylelint-prettier": "^5.0.
|
|
69
|
-
"ts-jest": "^29.
|
|
70
|
-
"ts-node": "^10.
|
|
71
|
-
"typescript": "^5.
|
|
64
|
+
"lint-staged": "^15.5.2",
|
|
65
|
+
"prettier": "^3.7.4",
|
|
66
|
+
"stylelint-config-standard": "^36.0.1",
|
|
67
|
+
"stylelint-prettier": "^5.0.3",
|
|
68
|
+
"ts-jest": "^29.4.6",
|
|
69
|
+
"ts-node": "^10.9.2",
|
|
70
|
+
"typescript": "^5.9.3"
|
|
72
71
|
},
|
|
73
72
|
"importSort": {
|
|
74
73
|
".js": {
|
|
@@ -76,4 +75,4 @@
|
|
|
76
75
|
"style": "module-grouping"
|
|
77
76
|
}
|
|
78
77
|
}
|
|
79
|
-
}
|
|
78
|
+
}
|
package/slice.d.ts
CHANGED
package/slice.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
1
5
|
export function createSliceManager(runtime) {
|
|
2
6
|
let docSlices = [];
|
|
3
7
|
// store slices in cache
|
|
@@ -22,17 +26,17 @@ export function createSliceManager(runtime) {
|
|
|
22
26
|
function setSliceAttrs(view) {
|
|
23
27
|
const result = getDocSlices();
|
|
24
28
|
let tr = view.state.tr;
|
|
25
|
-
|
|
29
|
+
for (const obj of result) {
|
|
26
30
|
view.state.doc.descendants((nodeactual, pos) => {
|
|
27
31
|
if (nodeactual?.attrs?.objectId === obj?.from) {
|
|
28
32
|
const newattrs = { ...nodeactual.attrs };
|
|
29
|
-
const isDeco = { ...
|
|
33
|
+
const isDeco = { ...newattrs.isDeco };
|
|
30
34
|
isDeco.isSlice = true;
|
|
31
35
|
newattrs.isDeco = isDeco;
|
|
32
36
|
tr = tr.setNodeMarkup(pos, undefined, newattrs);
|
|
33
37
|
}
|
|
34
38
|
});
|
|
35
|
-
}
|
|
39
|
+
}
|
|
36
40
|
view.dispatch(tr);
|
|
37
41
|
}
|
|
38
42
|
function addInfoIcon() {
|