@fastkit/vui 0.7.59 → 0.7.65

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.
Files changed (36) hide show
  1. package/dist/tool/index.js +1 -0
  2. package/dist/tool/vite-plugin.d.ts.map +1 -1
  3. package/dist/vui.cjs.js +616 -39
  4. package/dist/vui.cjs.prod.js +616 -39
  5. package/dist/vui.css +219 -0
  6. package/dist/vui.d.ts +141 -4
  7. package/dist/vui.min.css +1 -1
  8. package/dist/vui.min.css.map +1 -1
  9. package/dist/vui.mjs +606 -39
  10. package/package.json +7 -6
  11. package/src/components/VButton/VButtonGroup.scss +4 -0
  12. package/src/components/VButton/VButtonGroup.tsx +12 -1
  13. package/src/components/VWysiwygEditor/.DS_Store +0 -0
  14. package/src/components/VWysiwygEditor/VWysiwygEditor.scss +4 -0
  15. package/src/components/VWysiwygEditor/VWysiwygEditor.tsx +65 -22
  16. package/src/components/VWysiwygEditor/extensions/.DS_Store +0 -0
  17. package/src/components/VWysiwygEditor/extensions/color.ts +72 -0
  18. package/src/components/VWysiwygEditor/extensions/index.ts +2 -0
  19. package/src/components/VWysiwygEditor/extensions/linter/.DS_Store +0 -0
  20. package/src/components/VWysiwygEditor/extensions/linter/Linter.scss +140 -0
  21. package/src/components/VWysiwygEditor/extensions/linter/Linter.tsx +214 -0
  22. package/src/components/VWysiwygEditor/extensions/linter/LinterPlugin.ts +65 -0
  23. package/src/components/VWysiwygEditor/extensions/linter/index.ts +5 -0
  24. package/src/components/VWysiwygEditor/extensions/linter/plugins/BadWords.tsx +50 -0
  25. package/src/components/VWysiwygEditor/extensions/linter/plugins/HeadingLevel.ts +39 -0
  26. package/src/components/VWysiwygEditor/extensions/linter/plugins/Punctuation.ts +49 -0
  27. package/src/components/VWysiwygEditor/extensions/linter/plugins/index.ts +3 -0
  28. package/src/components/VWysiwygEditor/extensions/linter/utils.ts +28 -0
  29. package/src/components/VWysiwygEditor/index.ts +1 -0
  30. package/src/components/VWysiwygEditor/schemes.ts +190 -4
  31. package/src/components/VWysiwygEditor/tools/color.scss +140 -0
  32. package/src/components/VWysiwygEditor/tools/color.tsx +90 -0
  33. package/src/components/VWysiwygEditor/tools/index.ts +1 -1
  34. package/src/service.tsx +5 -0
  35. package/src/tool/vite-plugin.ts +1 -0
  36. package/src/components/VWysiwygEditor/tools/text-color.ts +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastkit/vui",
3
- "version": "0.7.59",
3
+ "version": "0.7.65",
4
4
  "description": "@fastkit/vui",
5
5
  "buildOptions": {
6
6
  "name": "Vui",
@@ -32,13 +32,14 @@
32
32
  "vue": "^3.2.26"
33
33
  },
34
34
  "dependencies": {
35
- "@fastkit/color-scheme": "0.7.59",
36
- "@fastkit/icon-font": "0.7.59",
37
- "@fastkit/tiny-logger": "0.7.59",
38
- "@fastkit/vite-kit": "0.7.59",
39
- "@fastkit/vue-kit": "0.7.59",
35
+ "@fastkit/color-scheme": "0.7.65",
36
+ "@fastkit/icon-font": "0.7.65",
37
+ "@fastkit/tiny-logger": "0.7.65",
38
+ "@fastkit/vite-kit": "0.7.65",
39
+ "@fastkit/vue-kit": "0.7.65",
40
40
  "@tiptap/extension-link": "^2.0.0-beta.36",
41
41
  "@tiptap/extension-underline": "^2.0.0-beta.23",
42
+ "@tiptap/extension-text-style": "^2.0.0-beta.23",
42
43
  "@tiptap/starter-kit": "^2.0.0-beta.183",
43
44
  "@tiptap/vue-3": "^2.0.0-beta.90",
44
45
  "eta": "^1.12.3",
@@ -20,4 +20,8 @@
20
20
  z-index: 1;
21
21
  }
22
22
  }
23
+
24
+ // &--has-icon &__item {
25
+
26
+ // }
23
27
  }
@@ -25,6 +25,7 @@ export const VButtonGroup = defineComponent({
25
25
  },
26
26
  setup(props, ctx) {
27
27
  return () => {
28
+ let hasIcon = false;
28
29
  let buttonLength = 0;
29
30
 
30
31
  let tmp = renderSlotOrEmpty(ctx.slots) || [];
@@ -43,6 +44,10 @@ export const VButtonGroup = defineComponent({
43
44
 
44
45
  buttonLength++;
45
46
 
47
+ if (!hasIcon && node.props && !!node.props.icon) {
48
+ hasIcon = true;
49
+ }
50
+
46
51
  return {
47
52
  isButton: true,
48
53
  node,
@@ -67,6 +72,7 @@ export const VButtonGroup = defineComponent({
67
72
  class: [
68
73
  'v-button-group__item',
69
74
  {
75
+ 'v-button--icon': hasIcon,
70
76
  'v-button-group__item--has-left': hasLeft,
71
77
  'v-button-group__item--has-right': hasRight,
72
78
  },
@@ -75,7 +81,12 @@ export const VButtonGroup = defineComponent({
75
81
  }
76
82
  return child.node;
77
83
  });
78
- return <div class="v-button-group">{$children}</div>;
84
+ return (
85
+ <div
86
+ class={['v-button-group', { 'v-button-group--has-icon': hasIcon }]}>
87
+ {$children}
88
+ </div>
89
+ );
79
90
  };
80
91
  },
81
92
  });
@@ -20,6 +20,10 @@
20
20
  position: relative;
21
21
  }
22
22
 
23
+ .v-control-field__body {
24
+ width: 100%;
25
+ }
26
+
23
27
  &__floating-toolbar {
24
28
  position: absolute;
25
29
  right: 0;
@@ -1,5 +1,13 @@
1
1
  import './VWysiwygEditor.scss';
2
- import { defineComponent, PropType, computed, ref, watch } from 'vue';
2
+ import {
3
+ defineComponent,
4
+ PropType,
5
+ computed,
6
+ ref,
7
+ watch,
8
+ onBeforeUnmount,
9
+ VNodeChild,
10
+ } from 'vue';
3
11
  import {
4
12
  createFormControlProps,
5
13
  FormControlSlots,
@@ -34,9 +42,13 @@ import {
34
42
  import StarterKit from '@tiptap/starter-kit';
35
43
  import { VButton, VButtonGroup } from '../VButton';
36
44
  import {
45
+ RawWysiwygExtension,
46
+ resolveRawWysiwygExtensions,
37
47
  RawWysiwygEditorTool,
38
48
  resolveRawWysiwygEditorTools,
39
49
  WysiwygEditorContext,
50
+ // EditorEventsOptions,
51
+ WysiwygEditorInitializeContext,
40
52
  } from './schemes';
41
53
 
42
54
  export const VWysiwygEditor = defineComponent({
@@ -49,6 +61,10 @@ export const VWysiwygEditor = defineComponent({
49
61
  ...createControlFieldProviderProps(),
50
62
  ...createControlProps(),
51
63
  ...defineSlotsProps<FormControlSlots & InputBoxSlots>(),
64
+ extensions: {
65
+ type: Array as PropType<RawWysiwygExtension[]>,
66
+ default: () => [],
67
+ },
52
68
  tools: {
53
69
  type: Array as PropType<RawWysiwygEditorTool[]>,
54
70
  default: () => [],
@@ -78,6 +94,23 @@ export const VWysiwygEditor = defineComponent({
78
94
  validationValue: () => textRef.value,
79
95
  });
80
96
 
97
+ const initializeCtx = new WysiwygEditorInitializeContext(() => vui, {
98
+ onCreate: ({ editor }) => {
99
+ textRef.value = editor.getText();
100
+ updateEditable();
101
+ },
102
+ onFocus: ({ event }) => {
103
+ inputControl.focusHandler(event);
104
+ },
105
+ onBlur: ({ event }) => {
106
+ inputControl.blurHandler(event);
107
+ },
108
+ onUpdate: ({ editor }) => {
109
+ inputControl.value = editor.getHTML();
110
+ textRef.value = editor.getText();
111
+ },
112
+ });
113
+
81
114
  const extensions = [
82
115
  StarterKit.configure({
83
116
  bold: false,
@@ -86,6 +119,10 @@ export const VWysiwygEditor = defineComponent({
86
119
  history: false,
87
120
  italic: false,
88
121
  }),
122
+ // Linter.configure({
123
+ // plugins: [BadWords(['abc', 'evidently']), Punctuation, HeadingLevel],
124
+ // }),
125
+ ...resolveRawWysiwygExtensions(props.extensions, initializeCtx),
89
126
  ...wysiwygSettings.value.extensions,
90
127
  ];
91
128
 
@@ -110,21 +147,13 @@ export const VWysiwygEditor = defineComponent({
110
147
  },
111
148
  );
112
149
 
150
+ initializeCtx.on('create', ({ editor }) => {
151
+ textRef.value = editor.getText();
152
+ updateEditable();
153
+ });
154
+
113
155
  const editor = useEditor({
114
- onCreate: ({ editor }) => {
115
- textRef.value = editor.getText();
116
- updateEditable();
117
- },
118
- onFocus: (ctx) => {
119
- inputControl.focusHandler(ctx.event);
120
- },
121
- onBlur: (ctx) => {
122
- inputControl.blurHandler(ctx.event);
123
- },
124
- onUpdate: (ev) => {
125
- inputControl.value = ev.editor.getHTML();
126
- textRef.value = ev.editor.getText();
127
- },
156
+ ...initializeCtx.editorOptions(),
128
157
  autofocus: props.autofocus,
129
158
  content: props.modelValue,
130
159
  extensions,
@@ -177,9 +206,12 @@ export const VWysiwygEditor = defineComponent({
177
206
 
178
207
  return (
179
208
  <VButtonGroup
180
- class={{
181
- 'v-wysiwyg-editor__floating-toolbar': props.floatingToolbar,
182
- }}
209
+ class={[
210
+ 'v-wysiwyg-editor__toolbar',
211
+ {
212
+ 'v-wysiwyg-editor__floating-toolbar': props.floatingToolbar,
213
+ },
214
+ ]}
183
215
  variant={variant}>
184
216
  {tools.map(({ key, icon, onClick, disabled, active }) => {
185
217
  const isActive =
@@ -188,16 +220,22 @@ export const VWysiwygEditor = defineComponent({
188
220
  !inputControl.canOperation ||
189
221
  (!!_editor && resolveContextableValue(context, disabled));
190
222
  let iconName: IconName | undefined;
223
+ let child: VNodeChild;
191
224
 
192
225
  if (typeof icon === 'function') {
193
226
  if (_editor) {
194
- iconName = icon(context);
227
+ const _child = icon(context);
228
+ if (typeof _child === 'function') {
229
+ child = _child();
230
+ } else {
231
+ iconName = child as any;
232
+ }
195
233
  }
196
234
  } else {
197
235
  iconName = icon;
198
236
  }
199
237
 
200
- if (!iconName) return;
238
+ if (!iconName && child == null) return;
201
239
 
202
240
  return (
203
241
  <VButton
@@ -206,14 +244,19 @@ export const VWysiwygEditor = defineComponent({
206
244
  icon={iconName}
207
245
  onClick={(ev) => onClick(context, ev)}
208
246
  color={isActive ? toolButtonActiveColor : undefined}
209
- disabled={isDisabled}
210
- />
247
+ disabled={isDisabled}>
248
+ {child}
249
+ </VButton>
211
250
  );
212
251
  })}
213
252
  </VButtonGroup>
214
253
  );
215
254
  };
216
255
 
256
+ onBeforeUnmount(() => {
257
+ editor.value && editor.value.destroy();
258
+ });
259
+
217
260
  return {
218
261
  editor,
219
262
  ...inputControl.expose(),
@@ -0,0 +1,72 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import '@tiptap/extension-text-style';
3
+
4
+ export type WysiwygColorOptions = {
5
+ types: string[];
6
+ };
7
+
8
+ declare module '@tiptap/core' {
9
+ interface Commands<ReturnType> {
10
+ color: {
11
+ /**
12
+ * Set the text color
13
+ */
14
+ setColor: (color: string) => ReturnType;
15
+ /**
16
+ * Unset the text color
17
+ */
18
+ unsetColor: () => ReturnType;
19
+ };
20
+ }
21
+ }
22
+
23
+ export const WysiwygColorExtension = Extension.create<WysiwygColorOptions>({
24
+ name: 'color',
25
+
26
+ addOptions() {
27
+ return {
28
+ types: ['textStyle'],
29
+ };
30
+ },
31
+
32
+ addGlobalAttributes() {
33
+ return [
34
+ {
35
+ types: this.options.types,
36
+ attributes: {
37
+ color: {
38
+ default: null,
39
+ parseHTML: (element) => element.style.color.replace(/['"]+/g, ''),
40
+ renderHTML: (attributes) => {
41
+ if (!attributes.color) {
42
+ return {};
43
+ }
44
+
45
+ return {
46
+ style: `color: ${attributes.color}`,
47
+ };
48
+ },
49
+ },
50
+ },
51
+ },
52
+ ];
53
+ },
54
+
55
+ addCommands() {
56
+ return {
57
+ setColor:
58
+ (color) =>
59
+ ({ chain }) => {
60
+ return chain().setMark('textStyle', { color }).run();
61
+ },
62
+ unsetColor:
63
+ () =>
64
+ ({ chain }) => {
65
+ return chain()
66
+ .setMark('textStyle', { color: null })
67
+ .removeEmptyTextStyle()
68
+ .run();
69
+ },
70
+ };
71
+ },
72
+ });
@@ -0,0 +1,2 @@
1
+ export * from './linter';
2
+ export * from './color';
@@ -0,0 +1,140 @@
1
+ .v-linter {
2
+ &__problem {
3
+ margin-bottom: -1px;
4
+ cursor: help;
5
+ background: var(--deep-color);
6
+ border-bottom: 1px solid var(--main-color);
7
+ }
8
+
9
+ &__issue-icon {
10
+ position: absolute;
11
+ right: 2px;
12
+ display: inline-flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ width: 1.2em;
16
+ height: 1.2em;
17
+ margin-top: 0.2em;
18
+ // padding-left: 0.5px;
19
+ font-family: times, georgia, serif;
20
+ font-size: 15px;
21
+ font-weight: 700;
22
+ line-height: 1;
23
+ color: var(--text-color);
24
+ text-align: center;
25
+ cursor: pointer;
26
+ background: var(--main-color);
27
+ border-radius: 100px;
28
+
29
+ &::before {
30
+ content: '!';
31
+ }
32
+ }
33
+
34
+ &__issue-menu-wrapper {
35
+ position: relative;
36
+ display: inline-block;
37
+ width: 0px;
38
+ height: 1em;
39
+ overflow: visible;
40
+ }
41
+
42
+ &__issue-menu {
43
+ font-size: 87.5%;
44
+ // font-size: 12px;
45
+ line-height: 1.5;
46
+
47
+ .v-dialog__body {
48
+ padding: 0;
49
+ font-size: inherit;
50
+ }
51
+
52
+ &__message {
53
+ padding: 8px;
54
+ }
55
+
56
+ &__fixers {
57
+ position: relative;
58
+ padding: 4px 0;
59
+ // padding: 8px;
60
+ // border-top: solid 1px var(--divider-color);
61
+ &::before {
62
+ position: absolute;
63
+ top: 0;
64
+ right: 0;
65
+ left: 0;
66
+ display: block;
67
+ height: 1px;
68
+ content: '';
69
+ background: currentColor;
70
+ opacity: 0.5;
71
+ }
72
+ }
73
+
74
+ &__fixer {
75
+ &__button {
76
+ position: relative;
77
+ display: block;
78
+ width: 100%;
79
+ padding: 8px;
80
+ font: inherit;
81
+ font-size: 12px;
82
+ // margin-top: 8px;
83
+ // color: var(--palette-link);
84
+ text-align: left;
85
+ // text-decoration: underline;
86
+ cursor: pointer;
87
+ background: transparent;
88
+ border: 0;
89
+ outline: 0;
90
+ appearance: none;
91
+
92
+ &::before {
93
+ position: absolute;
94
+ top: 0;
95
+ right: 0;
96
+ bottom: 0;
97
+ left: 0;
98
+ display: block;
99
+ content: '';
100
+ background: currentColor;
101
+ opacity: 0;
102
+ transition: 0.15s;
103
+ }
104
+
105
+ &:hover,
106
+ &:focus {
107
+ &::before {
108
+ opacity: 0.1;
109
+ }
110
+ }
111
+
112
+ &__message {
113
+ position: relative;
114
+ }
115
+ }
116
+ }
117
+ // pointer-events: none;
118
+ // background: #fff;
119
+ // border-radius: 4px;
120
+ // opacity: 0;
121
+ // transition: opacity 0.15s;
122
+
123
+ // &--active {
124
+ // pointer-events: auto;
125
+ // opacity: 1;
126
+ // }
127
+
128
+ &__fix {
129
+ display: inline;
130
+ margin-top: 8px;
131
+ color: var(--palette-link);
132
+ text-decoration: underline;
133
+ cursor: pointer;
134
+ background: transparent;
135
+ border: 0;
136
+ outline: 0;
137
+ appearance: none;
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,214 @@
1
+ import './Linter.scss';
2
+
3
+ import { Extension } from '@tiptap/core';
4
+ import { Decoration, DecorationSet } from 'prosemirror-view';
5
+ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
6
+ import { Node as ProsemirrorNode } from 'prosemirror-model';
7
+ import {
8
+ WysiwygLinterPlugin,
9
+ WysiwygLinterResult as Issue,
10
+ WysiwygLinterFixMessage,
11
+ } from './LinterPlugin';
12
+ import { EditorView } from 'prosemirror-view';
13
+ import { createWysiwygExtension } from '../../schemes';
14
+
15
+ const PROBLEM_CLASS_NAME = 'v-linter__problem';
16
+ const ISSUE_ICON_CLASS_NAME = 'v-linter__issue-icon';
17
+ const ISSUE_DATASET_NAME = 'data-issue-id';
18
+
19
+ function resolveWysiwygLinterFixMessage(message: WysiwygLinterFixMessage) {
20
+ return typeof message === 'function' ? message() : message;
21
+ }
22
+
23
+ function renderIcon(view: EditorView<any>, issue: Issue) {
24
+ const { level = 'warning' } = issue;
25
+ const icon = document.createElement('div');
26
+ const message = resolveWysiwygLinterFixMessage(issue.message);
27
+
28
+ icon.className = `${ISSUE_ICON_CLASS_NAME} ${level}-scope`;
29
+ icon.setAttribute(ISSUE_DATASET_NAME, issue.id);
30
+
31
+ if (typeof message === 'string') {
32
+ icon.title = message;
33
+ }
34
+
35
+ return icon;
36
+ }
37
+
38
+ function runAllLinterPlugins(
39
+ doc: ProsemirrorNode,
40
+ plugins: Array<typeof WysiwygLinterPlugin>,
41
+ ) {
42
+ const decorations: [any?] = [];
43
+
44
+ const issues = plugins
45
+ .map((RegisteredLinterPlugin) => {
46
+ return new RegisteredLinterPlugin(doc).scan().getResults();
47
+ })
48
+ .flat();
49
+
50
+ issues.forEach((issue) => {
51
+ const { level = 'warning', icon } = issue;
52
+ const message = resolveWysiwygLinterFixMessage(issue.message);
53
+ decorations.push(
54
+ Decoration.inline(issue.from, issue.to, {
55
+ class: `${PROBLEM_CLASS_NAME} ${level}-scope`,
56
+ 'data-issue-id': issue.id,
57
+ title: typeof message === 'string' ? message : undefined,
58
+ }),
59
+ );
60
+ if (icon) {
61
+ decorations.push(
62
+ Decoration.widget(issue.from, (view) => renderIcon(view, issue)),
63
+ );
64
+ }
65
+ });
66
+
67
+ const decorationSet = DecorationSet.create(doc, decorations);
68
+
69
+ return {
70
+ issues,
71
+ decorationSet,
72
+ };
73
+ }
74
+
75
+ export interface WysiwygLinterOptions {
76
+ plugins: Array<typeof WysiwygLinterPlugin>;
77
+ }
78
+
79
+ export interface WysiwygLinterStorage {
80
+ issues: Issue[];
81
+ }
82
+
83
+ export const WysiwygLinter = createWysiwygExtension((ctx) => {
84
+ function showMenu(view: EditorView<any>, issue: Issue, event: Event) {
85
+ const message = resolveWysiwygLinterFixMessage(issue.message);
86
+ const variant = ctx.vui.setting('containedVariant');
87
+ const scope = ctx.vui.setting(`${issue.level}Scope`);
88
+
89
+ return ctx.vui.menu({
90
+ activator: event,
91
+ content: (stack) => (
92
+ <div class="v-linter__issue-menu__body">
93
+ <div
94
+ class={[
95
+ 'v-linter__issue-menu__message',
96
+ `${scope}-scope ${variant}`,
97
+ ]}>
98
+ {message}
99
+ </div>
100
+ {issue.fix.length && (
101
+ <div class="v-linter__issue-menu__fixers">
102
+ {issue.fix.map((fixer, index) => (
103
+ <div key={index} class="v-linter__issue-menu__fixer">
104
+ <button
105
+ type="button"
106
+ class="v-linter__issue-menu__fixer__button"
107
+ onClick={() => {
108
+ stack.close();
109
+ fixer.handler(view, issue);
110
+ }}>
111
+ <span class="v-linter__issue-menu__fixer__button__message">
112
+ {resolveWysiwygLinterFixMessage(fixer.message)}
113
+ </span>
114
+ </button>
115
+ </div>
116
+ ))}
117
+ </div>
118
+ )}
119
+ </div>
120
+ ),
121
+ });
122
+ }
123
+
124
+ return Extension.create<WysiwygLinterOptions, WysiwygLinterStorage>({
125
+ name: 'linter',
126
+
127
+ addOptions() {
128
+ return {
129
+ plugins: [],
130
+ };
131
+ },
132
+ addStorage() {
133
+ return {
134
+ issues: [],
135
+ };
136
+ },
137
+ addProseMirrorPlugins() {
138
+ const { plugins } = this.options;
139
+ const { storage } = this;
140
+
141
+ const runAll = (doc: ProsemirrorNode) => {
142
+ const { issues, decorationSet } = runAllLinterPlugins(doc, plugins);
143
+ storage.issues = issues;
144
+ return decorationSet;
145
+ };
146
+
147
+ const getIssue = (id: string) =>
148
+ storage.issues.find((issue) => issue.id === id);
149
+
150
+ const getIssueElementByEvent = (
151
+ source: EventTarget | null,
152
+ ):
153
+ | {
154
+ issue: Issue;
155
+ }
156
+ | undefined => {
157
+ if (!source || !(source instanceof HTMLElement)) {
158
+ return;
159
+ }
160
+ const issueId = source.getAttribute(ISSUE_DATASET_NAME);
161
+ const issue = issueId && getIssue(issueId);
162
+ if (!issue) return;
163
+
164
+ return {
165
+ issue,
166
+ };
167
+ };
168
+
169
+ return [
170
+ new Plugin({
171
+ key: new PluginKey('linter'),
172
+ state: {
173
+ init(_, { doc }) {
174
+ return runAll(doc);
175
+ },
176
+ apply(transaction, oldState) {
177
+ return transaction.docChanged
178
+ ? runAll(transaction.doc)
179
+ : oldState;
180
+ },
181
+ },
182
+ props: {
183
+ decorations(state) {
184
+ return this.getState(state);
185
+ },
186
+ handleClick(view, _, event) {
187
+ const info = getIssueElementByEvent(event.target);
188
+ if (!info) {
189
+ return false;
190
+ }
191
+
192
+ const { issue } = info;
193
+ const { from, to } = issue;
194
+
195
+ const focus = () =>
196
+ view.dispatch(
197
+ view.state.tr
198
+ .setSelection(
199
+ TextSelection.create(view.state.doc, from, to),
200
+ )
201
+ .scrollIntoView(),
202
+ );
203
+
204
+ focus();
205
+
206
+ showMenu(view, issue, event);
207
+ return true;
208
+ },
209
+ },
210
+ }),
211
+ ];
212
+ },
213
+ });
214
+ });