@oxygen-cms/ui 1.7.2 → 1.8.0

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 (50) hide show
  1. package/{.eslintrc.js → .eslintrc.json} +3 -3
  2. package/package.json +14 -4
  3. package/src/CrudApi.js +23 -9
  4. package/src/MediaApi.js +3 -3
  5. package/src/PagesApi.js +48 -0
  6. package/src/PartialsApi.js +30 -0
  7. package/src/components/AuthenticatedLayout.vue +3 -1
  8. package/src/components/CodeEditor.vue +1 -1
  9. package/src/components/GroupsChooser.vue +2 -2
  10. package/src/components/GroupsList.vue +2 -2
  11. package/src/components/PageActions.vue +28 -0
  12. package/src/components/PageEdit.vue +164 -0
  13. package/src/components/PageNestedPagination.vue +27 -0
  14. package/src/components/PageNestedRow.vue +52 -0
  15. package/src/components/PageStatusIcon.vue +33 -0
  16. package/src/components/PageTable.vue +156 -0
  17. package/src/components/PartialActions.vue +28 -0
  18. package/src/components/PartialList.vue +74 -0
  19. package/src/components/PartialStatusIcon.vue +29 -0
  20. package/src/components/PartialTable.vue +65 -0
  21. package/src/components/ResourceList.vue +132 -0
  22. package/src/components/UserManagement.vue +2 -2
  23. package/src/components/UserProfileForm.vue +1 -1
  24. package/src/components/UserProfilePage.vue +1 -1
  25. package/src/components/content/CommandsList.vue +108 -0
  26. package/src/components/content/ContentEditor.vue +489 -0
  27. package/src/components/content/GridCellNodeView.vue +82 -0
  28. package/src/components/content/GridRowNodeView.vue +53 -0
  29. package/src/components/content/HtmlNodeView.vue +89 -0
  30. package/src/components/content/MarkMenu.vue +116 -0
  31. package/src/components/content/MediaNodeView.vue +83 -0
  32. package/src/components/content/ObjectLinkNodeView.vue +181 -0
  33. package/src/components/content/PartialNodeView.vue +217 -0
  34. package/src/components/content/commands.js +72 -0
  35. package/src/components/content/suggestion.js +211 -0
  36. package/src/components/media/MediaChooseDirectory.vue +2 -2
  37. package/src/components/media/MediaDirectory.vue +1 -1
  38. package/src/components/media/MediaInsertModal.vue +11 -2
  39. package/src/components/media/MediaItem.vue +1 -1
  40. package/src/components/media/MediaItemPreview.vue +18 -2
  41. package/src/components/media/MediaList.vue +4 -5
  42. package/src/components/media/MediaUpload.vue +1 -1
  43. package/src/components/media/media.scss +1 -0
  44. package/src/components/pages/PageList.vue +65 -0
  45. package/src/components/users/CreateUserModal.vue +1 -1
  46. package/src/components/util.css +1 -1
  47. package/src/icons.js +33 -5
  48. package/src/main.js +4 -0
  49. package/src/modules/PagesPartials.js +74 -2
  50. package/src/styles/pages-table.scss +34 -0
@@ -0,0 +1,489 @@
1
+ <template>
2
+ <div ref="container" class="editor">
3
+ <editor-content class="oxygen-editor-content" :editor="editor" />
4
+ <CommandsList ref="commandsList" :visible="commandsListVisible" :items="commandsListItems" :command="commandsListCommand" :top="commandsListTop - boundingClientRect().top" :left="commandsListLeft - boundingClientRect().left" />
5
+ <MarkMenu v-if="editor" :editor="editor" />
6
+ <floating-menu v-if="editor && isEditable" v-show="!commandsListVisible" :editor="editor" :should-show="shouldShowFloatingMenu" :tippy-options="{ popperOptions: { strategy: 'fixed', placement: 'left' }, getReferenceClientRect: getFloatingMenuClientRect, zIndex: 39 }" class="floating-menu-contents">
7
+ <b-dropdown ref="insertBlockDropdown" scrollable @active-change="insertBlockMenuActiveChange">
8
+ <template #trigger>
9
+ <b-button icon-left="plus" class="floating-menu-button first-child"></b-button>
10
+ </template>
11
+ <b-input ref="insertBlockSearch" v-model="commandSearchQuery" type="search" placeholder="Choose block to insert"></b-input>
12
+ <b-dropdown-item v-for="(item, index) in filteredNewBlocks" :key="index" @click="insertBlock(item)">
13
+ <b-icon :icon="item.icon"></b-icon>
14
+ {{ item.title }}
15
+ </b-dropdown-item>
16
+ <b-dropdown-item v-if="filteredNewBlocks.length === 0">
17
+ No blocks found.
18
+ </b-dropdown-item>
19
+ </b-dropdown>
20
+ <b-button icon-left="trash" class="floating-menu-button" @click="deleteBlock"></b-button>
21
+ <b-dropdown ref="convertBlockDropdown" scrollable @active-change="convertBlockMenuActiveChange">
22
+ <template #trigger>
23
+ <b-button :icon-left="currentActiveIcon" class="floating-menu-button last-child"></b-button>
24
+ </template>
25
+ <b-dropdown-item :paddingless="true">
26
+ <b-input ref="convertBlockSearch" v-model="commandSearchQuery" type="search" placeholder="Change current block"></b-input>
27
+ </b-dropdown-item>
28
+ <b-dropdown-item v-for="(item, index) in filteredConvertibleBlocks" :key="index" @click="convertBlock(item)">
29
+ <b-icon :icon="item.icon"></b-icon>
30
+ {{ item.title }}
31
+ </b-dropdown-item>
32
+ <b-dropdown-item v-if="filteredConvertibleBlocks.length === 0">
33
+ No blocks found.
34
+ </b-dropdown-item>
35
+ </b-dropdown>
36
+ </floating-menu>
37
+ </div>
38
+ </template>
39
+
40
+ <script>
41
+ import {Editor, EditorContent, FloatingMenu, Mark} from '@tiptap/vue-2'
42
+ import { VueNodeViewRenderer } from '@tiptap/vue-2'
43
+ import Underline from '@tiptap/extension-underline'
44
+ import Link from '@tiptap/extension-link'
45
+ import StarterKit from '@tiptap/starter-kit'
46
+ import { Node, mergeAttributes, posToDOMRect } from '@tiptap/core'
47
+ import MediaNodeView from "./MediaNodeView.vue";
48
+ import HtmlNodeView from "./HtmlNodeView.vue";
49
+ import GridRowNodeView from "./GridRowNodeView.vue";
50
+ import Commands from './commands'
51
+ import GridCellNodeView from "./GridCellNodeView.vue";
52
+ import ObjectLinkNodeView from "./ObjectLinkNodeView.vue";
53
+ import CommandsList from "./CommandsList.vue";
54
+ import {debounce} from "lodash";
55
+ import {items} from "./suggestion";
56
+ import MarkMenu from "./MarkMenu.vue";
57
+ import {FetchBuilder, getApiHost} from "../../api.js";
58
+ import {getApiRoot} from "../../CrudApi.js";
59
+
60
+ export default {
61
+ name: "ContentEditor",
62
+ components: {EditorContent, CommandsList, FloatingMenu, MarkMenu },
63
+ props: {
64
+ content: {
65
+ required: true
66
+ },
67
+ expanded: {
68
+ type: Boolean,
69
+ default: false
70
+ },
71
+ editable: {
72
+ type: Boolean,
73
+ default: true
74
+ }
75
+ },
76
+ data() {
77
+ return {
78
+ editor: null,
79
+ commandsListTop: 0,
80
+ commandsListLeft: 0,
81
+ commandsListVisible: false,
82
+ commandsListItems: [],
83
+ commandsListCommand: () => {},
84
+ commandSearchQuery: '',
85
+ currentActiveIcon: 'pencil-alt'
86
+ }
87
+ },
88
+ computed: {
89
+ isEditable() {
90
+ return this.editor ? this.editor.isEditable : false;
91
+ },
92
+ filteredConvertibleBlocks() {
93
+ return items({ query: this.commandSearchQuery, canConvert: true });
94
+ },
95
+ filteredNewBlocks() {
96
+ return items({ query: this.commandSearchQuery });
97
+ }
98
+ },
99
+ watch: {
100
+ 'editable': 'updateEditable',
101
+ 'content': 'setContent'
102
+ },
103
+ async mounted() {
104
+ const PartialNodeView = (await import("./PartialNodeView.vue")).default;
105
+
106
+ let extensions = [StarterKit,
107
+ Underline,
108
+ Link.configure({openOnClick: false}),
109
+ Commands.configure({editorComponent: this })];
110
+
111
+ const MediaItemNode = Node.create({
112
+ name: 'mediaItem',
113
+ group: 'block',
114
+ atom: true,
115
+ selectable: true,
116
+ draggable: true,
117
+ addAttributes() {
118
+ return {
119
+ id: {
120
+ default: null,
121
+ },
122
+ }
123
+ },
124
+ parseHTML() {
125
+ return [
126
+ {
127
+ tag: 'media-item',
128
+ },
129
+ ]
130
+ },
131
+ renderHTML({ HTMLAttributes }) {
132
+ return ['media-item', mergeAttributes(HTMLAttributes)]
133
+ },
134
+ addNodeView() {
135
+ return VueNodeViewRenderer(MediaNodeView)
136
+ },
137
+ });
138
+
139
+ const RawHtmlNode = Node.create({
140
+ name: 'rawHtml',
141
+ group: 'block',
142
+ atom: true,
143
+ draggable: true,
144
+ selectable: true,
145
+ addAttributes() {
146
+ return {
147
+ code: {
148
+ default: '',
149
+ },
150
+ }
151
+ },
152
+ parseHTML() {
153
+ return [
154
+ {
155
+ tag: 'raw-html',
156
+ },
157
+ ]
158
+ },
159
+ renderHTML({ HTMLAttributes }) {
160
+ return ['raw-html', mergeAttributes(HTMLAttributes)]
161
+ },
162
+ addNodeView() {
163
+ return VueNodeViewRenderer(HtmlNodeView)
164
+ },
165
+ });
166
+
167
+ const RowNode = Node.create({
168
+ name: 'gridRow',
169
+ group: 'block',
170
+ isolating: true,
171
+ selectable: true,
172
+ draggable: true,
173
+ content: 'gridCell*',
174
+ parseHTML() {
175
+ return [
176
+ {
177
+ tag: 'div',
178
+ getAttrs: element => element.classList.contains('Row') && null
179
+ },
180
+ ]
181
+ },
182
+ renderHTML({ HTMLAttributes }) {
183
+ return ['div', mergeAttributes(HTMLAttributes, {class: 'Row'}), 0]
184
+ },
185
+ addNodeView() {
186
+ return VueNodeViewRenderer(GridRowNodeView)
187
+ },
188
+ });
189
+
190
+ const CellNode = Node.create({
191
+ name: 'gridCell',
192
+ group: 'block',
193
+ isolating: true,
194
+ selectable: true,
195
+ draggable: true,
196
+ content: 'block*',
197
+ addAttributes() {
198
+ return {
199
+ cellType: {
200
+ default: 'wide',
201
+ renderHTML: (attributes) => {
202
+ return { 'class': attributes.cellType === 'wide' ? 'Cell Cell--wide' : 'Cell Cell--narrow' };
203
+ },
204
+ parseHTML: (element) => element.classList.contains('Cell--wide') ? 'wide' : 'narrow'
205
+ }
206
+ }
207
+ },
208
+ parseHTML() {
209
+ return [
210
+ {
211
+ tag: 'div',
212
+ getAttrs: element => element.classList.contains('Cell') && null
213
+ },
214
+ ]
215
+ },
216
+ renderHTML({ HTMLAttributes }) {
217
+ return ['div', mergeAttributes(HTMLAttributes), 0]
218
+ },
219
+ addNodeView() {
220
+ return VueNodeViewRenderer(GridCellNodeView)
221
+ },
222
+ });
223
+
224
+ const PartialNode = Node.create({
225
+ name: 'partial',
226
+ group: 'block',
227
+ atom: true,
228
+ isolating: true,
229
+ // draggable: true,
230
+ selectable: true,
231
+ addAttributes() {
232
+ return {
233
+ id: {
234
+ default: null,
235
+ renderHTML: (attributes) => {return {'data-partial-id': attributes.id}},
236
+ parseHTML: (element) => element.attrs['data-partial-id']
237
+ }
238
+ }
239
+ },
240
+ parseHTML() {
241
+ return [
242
+ {
243
+ tag: 'div',
244
+ getAttrs: element => element.hasAttribute('data-partial-id')
245
+ },
246
+ ]
247
+ },
248
+ renderHTML({HTMLAttributes}) {
249
+ return ['div', mergeAttributes(HTMLAttributes)];
250
+ },
251
+ addNodeView() {
252
+ return VueNodeViewRenderer(PartialNodeView)
253
+ },
254
+ });
255
+
256
+ const ObjectLinkNode = Node.create({
257
+ name: 'objectLink',
258
+ group: 'inline',
259
+ inline: true,
260
+ atom: true,
261
+ isolating: true,
262
+ selectable: true,
263
+ addAttributes() {
264
+ return {
265
+ id: {default: null},
266
+ type: {},
267
+ content: {default: null}
268
+ }
269
+ },
270
+ parseHTML() {
271
+ return [
272
+ {
273
+ tag: 'object-link'
274
+ },
275
+ ]
276
+ },
277
+ renderHTML({HTMLAttributes}) {
278
+ return ['object-link', mergeAttributes(HTMLAttributes)];
279
+ },
280
+ addNodeView() {
281
+ return VueNodeViewRenderer(ObjectLinkNodeView)
282
+ }
283
+ });
284
+
285
+ const BlockEmphasisNode = Node.create({
286
+ name: 'blockEmphasis',
287
+ content: 'inline+',
288
+ defining: true,
289
+ group: 'block',
290
+ parseHTML() {
291
+ return [
292
+ {
293
+ tag: 'div',
294
+ getAttrs: element => element.classList.contains('BlockEmphasis')
295
+ },
296
+ ]
297
+ },
298
+ renderHTML({HTMLAttributes}) {
299
+ return ['div', mergeAttributes(HTMLAttributes, {'class': 'BlockEmphasis'}), 0];
300
+ },
301
+ });
302
+
303
+ const SmallMark = Mark.create({
304
+ name: 'small',
305
+ parseHTML() {
306
+ return [
307
+ {
308
+ tag: 'small',
309
+ getAttrs: element => element.classList.contains('BlockEmphasis')
310
+ },
311
+ ]
312
+ },
313
+ renderHTML({ HTMLAttributes }) {
314
+ return ['small', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
315
+ },
316
+ });
317
+
318
+ extensions.push(MediaItemNode);
319
+ extensions.push(RawHtmlNode);
320
+ extensions.push(RowNode);
321
+ extensions.push(CellNode);
322
+ extensions.push(PartialNode);
323
+ extensions.push(ObjectLinkNode);
324
+ extensions.push(BlockEmphasisNode);
325
+ extensions.push(SmallMark);
326
+
327
+ this.setupStyles().then(() => {
328
+ console.log('instantiating editor with content', JSON.stringify(this.content));
329
+ this.editor = new Editor({
330
+ content: this.content,
331
+ editable: this.editable,
332
+ onUpdate: this.onUpdate.bind(this),
333
+ onSelectionUpdate: this.onSelectionUpdate.bind(this),
334
+ extensions: extensions
335
+ });
336
+ this.editor.state.doc.check();
337
+ })
338
+ },
339
+ beforeDestroy() {
340
+ if(this.editor) {
341
+ this.editor.destroy()
342
+ }
343
+ },
344
+ methods: {
345
+ async setupStyles() {
346
+ let themeDetails = await FetchBuilder.default(this.$buefy, 'get')
347
+ .fetch(getApiRoot() + 'theme/details');
348
+ const singletonId = "contentEditorStylesheet";
349
+ const element = document.createElement("link");
350
+ element.setAttribute("rel", "stylesheet");
351
+ element.setAttribute("type", "text/css");
352
+ element.setAttribute("href", getApiHost() + themeDetails.contentStylesheet);
353
+ element.setAttribute("id", singletonId);
354
+ if(!document.getElementById(singletonId)) {
355
+ document.getElementsByTagName("head")[0].appendChild(element);
356
+ }
357
+ },
358
+ onUpdate() {
359
+ this.onUpdateDebounced();
360
+ this.onSelectionUpdate();
361
+ },
362
+ onUpdateDebounced: debounce(function() {
363
+ this.$emit('update:content', this.editor.getJSON());
364
+ }, 1500, {leading:true}),
365
+ onSelectionUpdate() {
366
+ if(this.editor.isActive('bulletList'))
367
+ {
368
+ this.currentActiveIcon = 'list-ul';
369
+ } else if(this.editor.isActive('orderedList'))
370
+ {
371
+ this.currentActiveIcon = 'list-ol';
372
+ } else if(this.editor.isActive('heading'))
373
+ {
374
+ this.currentActiveIcon = 'heading';
375
+ }
376
+ else if(this.editor.isActive('blockquote')) {
377
+ this.currentActiveIcon = 'quote-left';
378
+ } else if(this.editor.isActive('horizontalRule'))
379
+ {
380
+ this.currentActiveIcon = 'ruler-horizontal';
381
+ }
382
+ else if(this.editor.isActive('paragraph')) {
383
+ this.currentActiveIcon = 'paragraph';
384
+ }
385
+ },
386
+ getJSON() {
387
+ console.log(JSON.stringify(this.editor.getJSON()));
388
+ },
389
+ getHTML() {
390
+ console.log(this.editor.getHTML());
391
+ },
392
+ boundingClientRect() {
393
+ if(!this.$refs.container) {
394
+ return { top: 0, left: 0 };
395
+ }
396
+ return this.$refs.container.getBoundingClientRect();
397
+ },
398
+ setContent(content) {
399
+ if(this.editor === null || JSON.stringify(this.editor.getJSON()) === JSON.stringify(content)) { return; }
400
+ console.log("overriding editor content with ", JSON.stringify(content));
401
+ this.editor.commands.setContent(content);
402
+ },
403
+ updateEditable(newEditable) {
404
+ console.log('updating editable state', newEditable);
405
+ this.editor.setEditable(newEditable);
406
+ },
407
+ shouldShowFloatingMenu({editor}) {
408
+ return editor.isActive('paragraph') || editor.isActive('bulletList') || editor.isActive('blockquote') || editor.isActive('heading') || editor.isActive('orderedList') || editor.isActive('horizontalRule');
409
+ },
410
+ getFloatingMenuClientRect() {
411
+ let from = this.editor.view.state.selection.from;
412
+ let to = this.editor.view.state.selection.to;
413
+ let domRect = posToDOMRect(this.editor.view, from, to);
414
+ const offset = 130;
415
+ domRect.left = this.$refs.container.getBoundingClientRect().left - offset;
416
+ domRect.right = domRect.left;
417
+ domRect.width = 0;
418
+ return { ... domRect };
419
+ },
420
+ convertBlockMenuActiveChange(active) {
421
+ if(active) {
422
+ this.commandSearchQuery = '';
423
+ this.$refs.convertBlockSearch.focus();
424
+ }
425
+ },
426
+ insertBlockMenuActiveChange(active) {
427
+ if(active) {
428
+ this.commandSearchQuery = '';
429
+ this.$refs.insertBlockSearch.focus();
430
+ }
431
+ },
432
+ convertBlock(item) {
433
+ let command = this.editor.chain().focus();
434
+ command = item.command({ editor: this.editor, range: null, command: command });
435
+ command.run();
436
+ },
437
+ insertBlock(item) {
438
+ let resolvedPos = this.editor.state.doc.resolve(this.editor.state.selection.to);
439
+ let afterCurrentNode = resolvedPos.posAtIndex(0, 1) + resolvedPos.node(1).nodeSize - 1;
440
+ let tr = this.editor.state.tr.insert(afterCurrentNode, item.node({editor: this.editor}));
441
+ this.editor.view.dispatch(tr);
442
+ this.editor.chain().focus().setTextSelection(afterCurrentNode + 1).run();
443
+ },
444
+ deleteBlock() {
445
+ let resolvedPos = this.editor.state.doc.resolve(this.editor.state.selection.to);
446
+ let currentNode = resolvedPos.posAtIndex(0, 1);
447
+ let afterCurrentNode = currentNode + resolvedPos.node(1).nodeSize;
448
+ let tr = this.editor.state.tr.delete(currentNode - 1, afterCurrentNode - 1);
449
+ this.editor.view.dispatch(tr);
450
+ this.editor.chain().focus().run();
451
+ }
452
+ }
453
+ }
454
+ </script>
455
+
456
+ <style scoped>
457
+ .editor {
458
+ /* prevent margin collapsing from editor contents */
459
+ overflow: visible;
460
+
461
+ position: relative;
462
+ background-color: #fff;
463
+ }
464
+
465
+ .oxygen-editor-content {
466
+ margin-bottom: 0;
467
+ }
468
+
469
+ .floating-menu-button:not(.first-child) {
470
+ border-top-left-radius: 0;
471
+ border-bottom-left-radius: 0;
472
+ }
473
+
474
+ .floating-menu-button:not(.last-child) {
475
+ border-top-right-radius: 0;
476
+ border-bottom-right-radius: 0;
477
+ }
478
+
479
+ .floating-menu-button:not(.first-child.last-child) {
480
+ margin-left: -1px;
481
+ margin-right: -1px;
482
+ }
483
+ </style>
484
+
485
+ <style>
486
+ .ProseMirror-focused {
487
+ outline: none;
488
+ }
489
+ </style>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <NodeViewWrapper :class="{'Cell': true, 'Cell--narrow': cellType === 'narrow', 'Cell--wide': cellType === 'wide', 'editable': editor.isEditable, 'selected': editor.isEditable && selectedOrChildSelected}">
3
+ <b-field v-if="editor.isEditable && selectedOrChildSelected" class="toolbar">
4
+ <p class="control">
5
+ <b-button icon-left="grip-vertical" size="is-small" data-drag-handle></b-button>
6
+ </p>
7
+ <p class="control"><b-button icon-left="trash" size="is-small" class="trash" @click="removeSelf"></b-button></p>
8
+ <p class="control"><b-button size="is-small" @click="toggleSize">Toggle size</b-button></p>
9
+ </b-field>
10
+ <NodeViewContent></NodeViewContent>
11
+ </NodeViewWrapper>
12
+ </template>
13
+
14
+ <script>
15
+ import { nodeViewProps, NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'
16
+ export default {
17
+ name: "GridCellNodeView",
18
+ components: { NodeViewWrapper, NodeViewContent },
19
+ props: nodeViewProps,
20
+ data() {
21
+ return {
22
+ }
23
+ },
24
+ computed: {
25
+ cellType() {
26
+ return this.node.attrs.cellType ?? 'narrow';
27
+ },
28
+ selectedOrChildSelected() {
29
+ if(this.selected) {return true;}
30
+ let anchor = this.editor.state.selection.$anchor;
31
+ for(let i = anchor.depth; i >= 0; i--) {
32
+ // console.log(JSON.stringify(this.node), JSON.stringify(anchor.node(i).toJSON()), this.node.eq(anchor.node(i)));
33
+ if(this.node.eq(anchor.node(i))) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ },
40
+ methods: {
41
+ removeSelf() {
42
+ this.deleteNode();
43
+ },
44
+ toggleSize() {
45
+ this.updateAttributes({
46
+ cellType: this.node.attrs.cellType === 'narrow' ? 'wide' : 'narrow'
47
+ });
48
+ }
49
+ }
50
+ }
51
+ </script>
52
+
53
+ <style scoped>
54
+ .Cell {
55
+ padding: 1rem;
56
+ margin: 1rem;
57
+ position: relative;
58
+ }
59
+
60
+ .Cell.editable {
61
+ outline: 1px dashed blue;
62
+ min-height: 4rem;
63
+ }
64
+ .Cell.selected {
65
+ outline: 2px solid blue;
66
+ }
67
+
68
+ .Cell .toolbar {
69
+ z-index: 8;
70
+ position: absolute;
71
+ top: 0.5rem;
72
+ right: 0.5rem;
73
+ }
74
+
75
+ .Cell--wide {
76
+ flex: 2;
77
+ }
78
+
79
+ .Cell--narrow {
80
+ flex: 1;
81
+ }
82
+ </style>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <NodeViewWrapper class="wrapper" :class="{editable: editor.isEditable}">
3
+ <b-field v-if="editor.isEditable" class="toolbar">
4
+ <p class="control"><b-button icon-left="trash" size="is-small" @click="removeSelf"></b-button></p>
5
+ <p class="control"><b-button icon-left="plus" size="is-small" @click="addCell"></b-button></p>
6
+ </b-field>
7
+ <NodeViewContent class="Row"></NodeViewContent>
8
+ </NodeViewWrapper>
9
+ </template>
10
+
11
+ <script>
12
+ import { nodeViewProps, NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'
13
+ export default {
14
+ name: "GridRowNodeView",
15
+ components: { NodeViewWrapper, NodeViewContent },
16
+ props: nodeViewProps,
17
+ data() {
18
+ return {
19
+ }
20
+ },
21
+ methods: {
22
+ removeSelf() {
23
+ this.deleteNode();
24
+ },
25
+ addCell() {
26
+ this.editor.commands.insertContentAt(this.getPos() + 1, {
27
+ type: 'gridCell'
28
+ })
29
+ }
30
+ }
31
+ }
32
+ </script>
33
+
34
+ <style scoped>
35
+ .wrapper {
36
+ position: relative;
37
+ }
38
+ .wrapper.editable {
39
+ min-height: 4rem;
40
+ outline: 2px dashed red;
41
+ }
42
+
43
+ .toolbar {
44
+ z-index: 8;
45
+ position: absolute;
46
+ top: 0.5rem;
47
+ right: 0.5rem;
48
+ }
49
+
50
+ .Row {
51
+ display: flex;
52
+ }
53
+ </style>