@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.
@@ -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 {};
@@ -1,18 +1,28 @@
1
- // A generic Floating Menu ProseMirror Plugin
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, atAnchorBottomLeft, } from '@modusoperandi/licit-ui-commands';
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?.map(tr.mapping, tr.doc) };
38
+ return { decorations: decos ? DecorationSet.prototype.map.call(decos, tr.mapping, tr.doc) : decos };
29
39
  }
30
- decos = decos.map(tr.mapping, tr.doc);
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
- return this.getState(state)?.decorations;
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.closest('.float-icon');
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.closest('.pm-hamburger-wrapper');
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(() => console.log('Rich content copied'))
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 instanceUrl = 'http://modusoperandi.com/editor/instance/';
125
- const referenceUrl = 'http://modusoperandi.com/ont/document#Reference_nodes';
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
- editorView.state.doc.nodesBetween(from, to, (node) => {
182
+ const paragraphEntries = [];
183
+ editorView.state.doc.nodesBetween(from, to, (node, pos) => {
144
184
  if (node.type.name === 'paragraph') {
145
- if (!firstParagraphText && node.textContent?.trim()) {
146
- firstParagraphText = node.textContent.trim();
147
- }
148
- if (node.attrs?.objectId) {
149
- objectIds.push(node.attrs.objectId);
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[objectIds.length - 1] : '';
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(() => console.log('Plain text copied!'))
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
- try {
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
- catch (jsonErr) {
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 ((parsed &&
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((node, pos) => {
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 = '&#xf097';
320
- SliceMark.onclick = () => console.log('Slice deco clicked');
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 = '&#xf02b;';
328
- TagMark.onclick = () => console.log('Tag deco clicked');
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 = '&#xf075;';
336
- CommentMark.onclick = () => console.log('Comment deco clicked');
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().then((hasPM) => {
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: atAnchorBottomLeft,
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 = { ...(newattrs.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 async function showReferences(view) {
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
+ }
@@ -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
- close?: (menuName: string) => void;
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: () => { createNewSlice(this.props.editorView); this.props.close?.('Create Slice'); } }),
35
- React.createElement(CustomButton, { label: "Insert Reference", onClick: () => { showReferences(this.props.editorView); this.props.close?.('Insert reference'); } }))));
36
- }
37
- closePopup(menuName) {
38
- this.props.close?.(menuName);
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
@@ -1 +1,5 @@
1
+ /**
2
+ * @license MIT
3
+ * @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
4
+ */
1
5
  export * from './FloatingMenuPlugin';
package/index.js CHANGED
@@ -1 +1,5 @@
1
+ /**
2
+ * @license MIT
3
+ * @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
4
+ */
1
5
  export * from './FloatingMenuPlugin';
package/model.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @license MIT
3
+ * @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
4
+ */
1
5
  export interface SliceModel {
2
6
  name: string;
3
7
  description: string;
package/model.js CHANGED
@@ -1 +1,5 @@
1
+ /**
2
+ * @license MIT
3
+ * @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
4
+ */
1
5
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modusoperandi/licit-floatingmenu",
3
- "version": "0.1.0",
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
- "@testing-library/jest-dom": "^6.4.1",
41
- "@testing-library/react": "^16.0.0",
42
- "@testing-library/user-event": "^14.4.3",
43
- "@types/jest": "^29.0.2",
44
- "@types/node": "^20.11.17",
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.0.21",
47
- "@types/react-dom": "^18.0.6",
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": "^8.56.0",
53
- "eslint-config-prettier": "^9.1.0",
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.3.1",
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.4",
63
- "jest-prosemirror": "^2.1.5",
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.1",
66
- "prettier": "^3.0.0",
67
- "stylelint-config-standard": "^36.0.0",
68
- "stylelint-prettier": "^5.0.0",
69
- "ts-jest": "^29.0.7",
70
- "ts-node": "^10.4.0",
71
- "typescript": "^5.3.3"
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
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @license MIT
3
+ * @copyright Copyright 2025 Modus Operandi Inc. All Rights Reserved.
4
+ */
1
5
  import { EditorView } from 'prosemirror-view';
2
6
  import { EditorState } from 'prosemirror-state';
3
7
  import { FloatRuntime, SliceModel } from './model';
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
- result.forEach((obj) => {
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 = { ...(newattrs.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() {
package/ui/menu.css CHANGED
@@ -23,8 +23,7 @@
23
23
  opacity: 1;
24
24
  }
25
25
 
26
- .context-menu {
27
- position: absolute; /* positioned by createPopUp */
26
+ .context-menu {
28
27
  background: white;
29
28
  border: 1px solid #ccc;
30
29
  border-radius: 6px;