@modusoperandi/licit 1.3.1 → 1.3.2

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,33 +1,44 @@
1
1
  // @flow
2
2
 
3
- import {
4
- EditorState,
5
- Plugin,
6
- PluginKey,
7
- TextSelection,
8
- } from 'prosemirror-state';
3
+ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
9
4
  import { EditorView } from 'prosemirror-view';
10
5
 
11
6
  import { MARK_LINK } from './MarkNames.js';
12
- import { hideSelectionPlaceholder } from './SelectionPlaceholderPlugin.js';
13
7
  import {
14
- applyMark,
15
- findNodesWithSameMark,
16
8
  atAnchorTopCenter,
17
9
  createPopUp,
10
+ findNodesWithSameMark,
18
11
  } from '@modusoperandi/licit-ui-commands';
19
12
  import lookUpElement from './lookUpElement.js';
20
13
  import LinkTooltip from './ui/LinkTooltip.js';
21
- import LinkURLEditor from './ui/LinkURLEditor.js';
22
- import { INNER_LINK } from './Types.js';
14
+ import scrollIntoView from 'smooth-scroll-into-view-if-needed';
15
+
16
+ import sanitizeURL from './sanitizeURL.js';
17
+ import '@modusoperandi/licit-ui-commands/ui/czi-pop-up.css';
23
18
 
24
- // https://prosemirror.net/examples/tooltip/
25
19
  const SPEC = {
26
- // [FS] IRAD-1005 2020-07-07
27
- // Upgrade outdated packages.
28
20
  key: new PluginKey('LinkTooltipPlugin'),
21
+ props: {
22
+ handleDOMEvents: {
23
+ mouseover(view, event) {
24
+ const pluginView = view.dom._linkTooltipView;
25
+ return pluginView?._handleMouseOver(event) ?? false;
26
+ },
27
+ mouseout(view, event) {
28
+ const pluginView = view.dom._linkTooltipView;
29
+ return pluginView?._handleMouseOut.call(view, event);
30
+ },
31
+ click(view, event) {
32
+ const pluginView = view.dom._linkTooltipView;
33
+ return pluginView?._handleClick.call(view, event);
34
+ },
35
+ },
36
+ },
29
37
  view(editorView: EditorView) {
30
- return new LinkTooltipView(editorView);
38
+ const pluginView = new LinkTooltipView(editorView);
39
+ // Store the instance for use in DOM handlers
40
+ editorView.dom._linkTooltipView = pluginView;
41
+ return pluginView;
31
42
  },
32
43
  };
33
44
 
@@ -36,225 +47,159 @@ class LinkTooltipPlugin extends Plugin {
36
47
  super(SPEC);
37
48
  }
38
49
  }
39
-
40
50
  class LinkTooltipView {
41
- _anchorEl = null;
42
51
  _popup = null;
43
- _editor = null;
52
+ _anchorEl = null;
53
+ _view = null;
44
54
 
45
- constructor(editorView: EditorView) {
46
- this.update(editorView, null);
55
+ constructor(view) {
56
+ this._view = view;
47
57
  }
48
58
 
49
- getInnerlinkSelected_position(view: EditorView, selectionId): void {
50
- let tocItemPos = null;
51
- if (selectionId) {
52
- view.state.tr.doc.descendants((node, pos) => {
53
- if (node.attrs.styleName && node.attrs.innerLink === selectionId) {
54
- tocItemPos = { position: pos, textContent: node.textContent };
55
- }
56
- });
57
- }
58
- return tocItemPos;
59
- }
59
+ _handleMouseOver(event) {
60
+ const anchor = event.target?.closest('a');
61
+ if (!anchor || anchor === this._anchorEl) return;
60
62
 
61
- update(view: EditorView, lastState: EditorState): void {
62
- const { state } = view;
63
+ this._anchorEl = anchor;
64
+ const href = anchor.getAttribute('href');
65
+ this._popup?.close();
66
+ this._popup = createPopUp(
67
+ LinkTooltip,
68
+ {
69
+ href,
70
+ selectionId: null,
71
+ editorView: this._view,
72
+ },
73
+ {
74
+ anchor,
75
+ autoDismiss: true,
76
+ onClose: () => {
77
+ this._popup = null;
78
+ this._anchorEl = null;
79
+ },
80
+ position: atAnchorTopCenter,
81
+ }
82
+ );
83
+ }
84
+ _handleClick(event) {
85
+ const { state } = this.dom._linkTooltipView._view;
63
86
  const { doc, selection, schema } = state;
64
- const markType = schema.marks[MARK_LINK];
65
- if (!markType) {
66
- return;
67
- }
68
87
  const { from, to } = selection;
69
- const result = findNodesWithSameMark(doc, from, to, markType);
88
+ const markType = schema.marks[MARK_LINK];
70
89
 
90
+ const result = findNodesWithSameMark(doc, from, to, markType);
71
91
  if (!result) {
72
- this.destroy();
73
- return;
92
+ return false;
74
93
  }
75
- const domFound = view.domAtPos(from);
94
+ const domFound = this.dom._linkTooltipView._view.domAtPos(from);
76
95
  if (!domFound) {
77
- this.destroy();
78
- return;
96
+ return false;
79
97
  }
80
- const anchorEl = lookUpElement(domFound.node, (el) => el.nodeName === 'A');
81
- if (!anchorEl) {
82
- this.destroy();
83
- return;
98
+
99
+ const anchor = lookUpElement(domFound.node, (el) => el.nodeName === 'A');
100
+ if (!anchor) {
101
+ return false;
84
102
  }
85
- const tocItemPos = this.getInnerlinkSelected_position(
86
- view,
103
+
104
+ const href = anchor.getAttribute('href');
105
+ const selectionId = anchor.getAttribute('selectionid');
106
+
107
+ const tocItemPos = this.dom._linkTooltipView.getInnerlinkSelected_position(
108
+ this.dom._linkTooltipView._view,
87
109
  result.mark.attrs.selectionId
88
110
  );
89
- const popup = this._popup;
90
- const viewPops = {
91
- editorState: state,
92
- editorView: view,
93
- href: result.mark.attrs.href,
94
- selectionId_: result.mark.attrs.selectionId,
95
- onCancel: this._onCancel,
96
- onEdit: this._onEdit,
97
- onRemove: this._onRemove,
98
- tocItemPos_: tocItemPos,
99
- };
100
111
 
101
- if (popup && anchorEl === this._anchorEl) {
102
- popup.update(viewPops);
103
- } else {
104
- popup?.close();
105
- this._anchorEl = anchorEl;
106
- this._popup = createPopUp(LinkTooltip, viewPops, {
107
- anchor: anchorEl,
108
- autoDismiss: false,
109
- onClose: this._onClose,
110
- position: atAnchorTopCenter,
111
- });
112
+ this.dom._linkTooltipView.jumpLink(
113
+ this.dom._linkTooltipView._view,
114
+ tocItemPos,
115
+ href,
116
+ selectionId
117
+ );
118
+ event.preventDefault(); // prevent default browser navigation
119
+ return true;
120
+ }
121
+
122
+ _handleMouseOut(event) {
123
+ if (this._anchorEl && !this._anchorEl.contains(event.relatedTarget)) {
124
+ this._popup?.close();
125
+ this._popup = null;
126
+ this._anchorEl = null;
112
127
  }
113
128
  }
114
129
 
115
130
  destroy() {
116
131
  this._popup?.close();
117
- this._editor?.close();
118
132
  }
119
133
 
120
- _onCancel = (view: EditorView): void => {
121
- this.destroy();
122
- view.focus();
123
- };
124
-
125
- _onClose = (): void => {
126
- this._anchorEl = null;
127
- this._editor = null;
128
- this._popup = null;
129
- };
130
-
131
- showTocList = async (view) => {
132
- let storeTOCvalue = [];
133
- const TOCselectedNode = [];
134
-
135
- const stylePromise = view.runtime;
136
- if (stylePromise === null || undefined) {
137
- return TOCselectedNode;
134
+ jumpLink = (view: EditorView, tocItemPos, href, selectionId): void => {
135
+ if (selectionId || (selectionId === 0 && tocItemPos)) {
136
+ this.jumpInnerLink(view, tocItemPos);
138
137
  } else {
139
- const styles = await stylePromise.fetchStyles();
140
-
141
- storeTOCvalue = styles
142
- .filter(
143
- (
144
- style // Added TOT/TOF selected styles to be listed as well
145
- ) =>
146
- style?.styles?.toc === true ||
147
- style?.styles?.tot === true ||
148
- style?.styles?.tof === true
149
- )
150
- .map((style) => style?.styleName);
151
- view.state.tr.doc.descendants((node, pos) => {
152
- if (node.attrs.styleName) {
153
- for (let i = 0; i <= storeTOCvalue.length; i++) {
154
- if (storeTOCvalue[i] === node.attrs.styleName) {
155
- TOCselectedNode.push({ node_: node, pos_: pos });
156
- }
157
- }
158
- }
159
- });
160
- return TOCselectedNode;
138
+ this._openLink(href);
161
139
  }
162
140
  };
163
141
 
164
- _onEdit = (view: EditorView): void => {
165
- this._popup.close();
166
- if (this._editor) {
167
- return;
142
+ jumpInnerLink = (view: EditorView, tocItemPos): void => {
143
+ const transaction = view.state.tr;
144
+ const tr = transaction.setSelection(
145
+ TextSelection.create(transaction.doc, tocItemPos.position + 1)
146
+ );
147
+ view.dispatch(tr.scrollIntoView(true));
148
+ const dom = view.domAtPos(tocItemPos?.position + 1).node;
149
+ if (dom?.scrollIntoView) {
150
+ dom.scrollIntoView({ behavior: 'smooth', block: 'start' });
168
151
  }
152
+ };
169
153
 
170
- const { state } = view;
171
- const { schema, doc, selection } = state;
172
- const { from, to } = selection;
173
- const markType = schema.marks[MARK_LINK];
174
- const result = findNodesWithSameMark(doc, from, to, markType);
175
- if (!result) {
154
+ _openLink = (href: string): void => {
155
+ if (this.isBookMarkHref(href)) {
156
+ const id = href.substr(1);
157
+ const el = document.getElementById(id);
158
+ if (el) {
159
+ const { onCancel, editorView } = this.props;
160
+ onCancel(editorView);
161
+ (async () => {
162
+ // https://www.npmjs.com/package/smooth-scroll-into-view-if-needed
163
+ await scrollIntoView(el, {
164
+ scrollMode: 'if-needed',
165
+ behavior: 'smooth',
166
+ });
167
+ })();
168
+ }
176
169
  return;
177
170
  }
171
+ if (href) {
172
+ const url = sanitizeURL(href);
173
+ let popupString;
174
+
175
+ if (this._view.editable) {
176
+ popupString = 'Any unsaved changes will be lost';
177
+ } else {
178
+ popupString = '';
179
+ }
178
180
 
179
- this.showTocList(view).then((data) => {
180
- const tocItemsNode = data;
181
- const href = result.mark.attrs.href;
182
- const viewPops = {
183
- selectionId_: result.mark.attrs.selectionId,
184
- href_: href,
185
- TOCselectedNode_: tocItemsNode,
186
- view_: view,
187
- };
188
-
189
- this._editor = createPopUp(LinkURLEditor, viewPops, {
190
- onClose: (value) => {
191
- this._editor = null;
192
- this._onEditEnd(view, selection, value);
193
- },
194
- });
195
- });
181
+ if (this._view?.runtime?.openLinkDialog) {
182
+ this._view.runtime.openLinkDialog(url, popupString);
183
+ } else {
184
+ window.open(url);
185
+ }
186
+ }
196
187
  };
197
188
 
198
- _onRemove = (view: EditorView): void => {
199
- this._popup.close();
200
- this._onEditEnd(view, view.state.selection, null);
189
+ isBookMarkHref = (href: string): boolean => {
190
+ return !!href && href.startsWith('#') && href.length >= 2;
201
191
  };
202
192
 
203
- _onEditEnd = (
204
- view: EditorView,
205
- initialSelection: TextSelection,
206
- url: ?string
207
- ): void => {
208
- const { state, dispatch } = view;
209
- let tr = hideSelectionPlaceholder(state);
210
-
211
- if (url !== undefined) {
212
- const { schema } = state;
213
- const markType = schema.marks[MARK_LINK];
214
- if (markType) {
215
- const result = findNodesWithSameMark(
216
- tr.doc,
217
- initialSelection.from,
218
- initialSelection.to,
219
- markType
220
- );
221
- if (result) {
222
- const linkSelection = TextSelection.create(
223
- tr.doc,
224
- result.from.pos,
225
- result.to.pos + 1
226
- );
227
- tr = tr.setSelection(linkSelection);
228
- let selectionId;
229
- let href;
230
- if (url === null) {
231
- selectionId = null;
232
- href = null;
233
- } else if (url.includes(INNER_LINK)) {
234
- selectionId = url.split(INNER_LINK)[0];
235
- href = url.split(INNER_LINK)[1];
236
- } else {
237
- selectionId = null;
238
- href = url;
239
- }
240
-
241
- const attrs = href ? { href, selectionId } : null;
242
- tr = applyMark(tr, schema, markType, attrs);
243
-
244
- // [FS] IRAD-1005 2020-07-09
245
- // Upgrade outdated packages.
246
- // reset selection to original using the latest doc.
247
- const origSelection = TextSelection.create(
248
- tr.doc,
249
- initialSelection.from,
250
- initialSelection.to
251
- );
252
- tr = tr.setSelection(origSelection);
193
+ getInnerlinkSelected_position = (view: EditorView, selectionId): void => {
194
+ let tocItemPos = null;
195
+ if (selectionId) {
196
+ view.state.tr.doc.descendants((node, pos) => {
197
+ if (node.attrs.styleName && node.attrs.innerLink === selectionId) {
198
+ tocItemPos = { position: pos, textContent: node.textContent };
253
199
  }
254
- }
200
+ });
255
201
  }
256
- dispatch(tr);
257
- view.focus();
202
+ return tocItemPos;
258
203
  };
259
204
  }
260
205