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