@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.
- package/{.eslintrc.js → .eslintrc.json} +3 -3
- package/package.json +14 -4
- package/src/CrudApi.js +23 -9
- package/src/MediaApi.js +3 -3
- package/src/PagesApi.js +48 -0
- package/src/PartialsApi.js +30 -0
- package/src/components/AuthenticatedLayout.vue +3 -1
- package/src/components/CodeEditor.vue +1 -1
- package/src/components/GroupsChooser.vue +2 -2
- package/src/components/GroupsList.vue +2 -2
- package/src/components/PageActions.vue +28 -0
- package/src/components/PageEdit.vue +164 -0
- package/src/components/PageNestedPagination.vue +27 -0
- package/src/components/PageNestedRow.vue +52 -0
- package/src/components/PageStatusIcon.vue +33 -0
- package/src/components/PageTable.vue +156 -0
- package/src/components/PartialActions.vue +28 -0
- package/src/components/PartialList.vue +74 -0
- package/src/components/PartialStatusIcon.vue +29 -0
- package/src/components/PartialTable.vue +65 -0
- package/src/components/ResourceList.vue +132 -0
- package/src/components/UserManagement.vue +2 -2
- package/src/components/UserProfileForm.vue +1 -1
- package/src/components/UserProfilePage.vue +1 -1
- package/src/components/content/CommandsList.vue +108 -0
- package/src/components/content/ContentEditor.vue +489 -0
- package/src/components/content/GridCellNodeView.vue +82 -0
- package/src/components/content/GridRowNodeView.vue +53 -0
- package/src/components/content/HtmlNodeView.vue +89 -0
- package/src/components/content/MarkMenu.vue +116 -0
- package/src/components/content/MediaNodeView.vue +83 -0
- package/src/components/content/ObjectLinkNodeView.vue +181 -0
- package/src/components/content/PartialNodeView.vue +217 -0
- package/src/components/content/commands.js +72 -0
- package/src/components/content/suggestion.js +211 -0
- package/src/components/media/MediaChooseDirectory.vue +2 -2
- package/src/components/media/MediaDirectory.vue +1 -1
- package/src/components/media/MediaInsertModal.vue +11 -2
- package/src/components/media/MediaItem.vue +1 -1
- package/src/components/media/MediaItemPreview.vue +18 -2
- package/src/components/media/MediaList.vue +4 -5
- package/src/components/media/MediaUpload.vue +1 -1
- package/src/components/media/media.scss +1 -0
- package/src/components/pages/PageList.vue +65 -0
- package/src/components/users/CreateUserModal.vue +1 -1
- package/src/components/util.css +1 -1
- package/src/icons.js +33 -5
- package/src/main.js +4 -0
- package/src/modules/PagesPartials.js +74 -2
- package/src/styles/pages-table.scss +34 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NodeViewWrapper class="wrapper" :class="{editable: isEditable, selected: isEditable && selectedOrEditorFocused}">
|
|
3
|
+
<div v-if="isEditable" class="toolbar">
|
|
4
|
+
<div class="my-field has-addons" style="width: 100%">
|
|
5
|
+
<p v-if="!partial" class="control">
|
|
6
|
+
<b-button size="is-small" @click="partialChooserActive = true">Choose an existing partial</b-button>
|
|
7
|
+
</p>
|
|
8
|
+
<p v-if="!partial" class="control">
|
|
9
|
+
<b-button size="is-small" @click="createPartialModalActive = true">Create from scratch</b-button>
|
|
10
|
+
</p>
|
|
11
|
+
<div class="is-flex-grow-1"></div>
|
|
12
|
+
<p v-if="partial" class="control">
|
|
13
|
+
<b-button size="is-small" type="is-dark" @click="partialChooserActive = true">{{ partial.key }}</b-button>
|
|
14
|
+
</p>
|
|
15
|
+
<!-- <p class="control">-->
|
|
16
|
+
<!-- <b-button size="is-small" data-drag-handle type="is-dark" icon-left="grip-vertical"></b-button>-->
|
|
17
|
+
<!-- </p>-->
|
|
18
|
+
<p v-if="partial" class="control">
|
|
19
|
+
<b-button size="is-small" :disabled="!saveButtonEnabled" icon-left="save" type="is-dark" @click="savePartial"></b-button>
|
|
20
|
+
</p>
|
|
21
|
+
<p v-if="partial" class="control">
|
|
22
|
+
<b-dropdown position="is-bottom-left">
|
|
23
|
+
<template #trigger>
|
|
24
|
+
<b-button icon-left="sliders-h" size="is-small" type="is-dark"></b-button>
|
|
25
|
+
</template>
|
|
26
|
+
<div class="modal-card">
|
|
27
|
+
<header class="modal-card-head">
|
|
28
|
+
<p class="modal-card-title">Edit Partial Settings</p>
|
|
29
|
+
</header>
|
|
30
|
+
<div v-if="partial" class="modal-card-body">
|
|
31
|
+
<!-- <h2></h2>-->
|
|
32
|
+
<b-field label="Title" label-position="on-border">
|
|
33
|
+
<b-input :value="partial.title" @input="v => partial.title = v"></b-input>
|
|
34
|
+
</b-field>
|
|
35
|
+
<br>
|
|
36
|
+
<b-field label="Slug" label-position="on-border">
|
|
37
|
+
<b-input :value="partial.key" @input="v => partial.key = v"></b-input>
|
|
38
|
+
</b-field>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</b-dropdown>
|
|
42
|
+
</p>
|
|
43
|
+
<p class="control"><b-button icon-left="trash" size="is-small" type="is-danger" @click="removeSelf"></b-button></p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<b-modal :active="partialChooserActive" has-modal-card aria-role="dialog" aria-modal auto-focus @update:active="(v) => partialChooserActive = v">
|
|
47
|
+
<div class="modal-card">
|
|
48
|
+
<header class="modal-card-head">
|
|
49
|
+
<p class="modal-card-title">Choose a partial to use</p>
|
|
50
|
+
</header>
|
|
51
|
+
<section class="modal-card-body">
|
|
52
|
+
<PartialList :in-trash="false" search-query="" @choose="selectPartial" />
|
|
53
|
+
</section>
|
|
54
|
+
<footer class="modal-card-foot"></footer>
|
|
55
|
+
</div>
|
|
56
|
+
</b-modal>
|
|
57
|
+
<b-modal :active="createPartialModalActive" has-modal-card aria-role="dialog" aria-modal auto-focus @update:active="(v) => createPartialModalActive = v">
|
|
58
|
+
<div class="modal-card">
|
|
59
|
+
<header class="modal-card-head">
|
|
60
|
+
<p class="modal-card-title">Partial</p>
|
|
61
|
+
</header>
|
|
62
|
+
<section class="modal-card-body">
|
|
63
|
+
<p>A partial represents a fragment of reusable content which can be included in multiple places and/or on multiple pages.</p>
|
|
64
|
+
<hr>
|
|
65
|
+
<b-field label="Title" label-position="on-border">
|
|
66
|
+
<b-input v-model="title" />
|
|
67
|
+
</b-field>
|
|
68
|
+
<br>
|
|
69
|
+
<b-field label="Slug" label-position="on-border">
|
|
70
|
+
<b-input v-model="slug" />
|
|
71
|
+
</b-field>
|
|
72
|
+
</section>
|
|
73
|
+
<footer class="modal-card-foot is-flex">
|
|
74
|
+
<div class="is-flex-grow-1"></div>
|
|
75
|
+
<b-button @click="createPartialModalActive = false">Close</b-button>
|
|
76
|
+
<b-button type="is-success" @click="createPartial">
|
|
77
|
+
Create Partial
|
|
78
|
+
</b-button>
|
|
79
|
+
</footer>
|
|
80
|
+
</div>
|
|
81
|
+
</b-modal>
|
|
82
|
+
<ContentEditor v-if="partial !== null" ref="editor" :editable="isEditable" :content="partial.richContent" class="editor-content" @update:content="updateContent" />
|
|
83
|
+
<em v-else-if="!isEditable">Unlinked partial block...</em>
|
|
84
|
+
</NodeViewWrapper>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script>
|
|
88
|
+
import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-2'
|
|
89
|
+
import ContentEditor from "./ContentEditor.vue";
|
|
90
|
+
import PartialsApi from "../../PartialsApi.js";
|
|
91
|
+
import PartialList from "../PartialList.vue";
|
|
92
|
+
|
|
93
|
+
export default {
|
|
94
|
+
name: "PartialNodeView",
|
|
95
|
+
components: {ContentEditor, NodeViewWrapper, PartialList },
|
|
96
|
+
props: nodeViewProps,
|
|
97
|
+
data() {
|
|
98
|
+
return {
|
|
99
|
+
partialsApi: new PartialsApi(),
|
|
100
|
+
partialChooserActive: false,
|
|
101
|
+
createPartialModalActive: false,
|
|
102
|
+
partial: null,
|
|
103
|
+
serverPartial: null,
|
|
104
|
+
slug: 'layout.nav',
|
|
105
|
+
title: 'Navigation',
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
computed: {
|
|
109
|
+
isEditable() {
|
|
110
|
+
return this.editor.isEditable;
|
|
111
|
+
},
|
|
112
|
+
saveButtonEnabled() {
|
|
113
|
+
return this.partial && (JSON.stringify(this.partial) !== JSON.stringify(this.serverPartial));
|
|
114
|
+
},
|
|
115
|
+
selectedOrEditorFocused() {
|
|
116
|
+
return this.selected || (this.$refs.editor && this.$refs.editor.editor.isFocused);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
watch: {
|
|
120
|
+
'node': 'loadPartial'
|
|
121
|
+
},
|
|
122
|
+
mounted() {
|
|
123
|
+
this.loadPartial()
|
|
124
|
+
},
|
|
125
|
+
methods: {
|
|
126
|
+
async loadPartial() {
|
|
127
|
+
if(this.node.attrs.id === null) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let response = await this.partialsApi.get(this.node.attrs.id);
|
|
131
|
+
this.selectPartial(response.item);
|
|
132
|
+
},
|
|
133
|
+
removeSelf() {
|
|
134
|
+
this.deleteNode();
|
|
135
|
+
},
|
|
136
|
+
async createPartial() {
|
|
137
|
+
let response = await this.partialsApi.create({
|
|
138
|
+
key: this.slug,
|
|
139
|
+
title: this.title
|
|
140
|
+
});
|
|
141
|
+
if(!response.item) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.createPartialModalActive = false;
|
|
145
|
+
this.selectPartial(response.item);
|
|
146
|
+
},
|
|
147
|
+
selectPartial(partial) {
|
|
148
|
+
console.log('select partial', partial);
|
|
149
|
+
this.partial = { ...partial };
|
|
150
|
+
this.serverPartial = { ... partial };
|
|
151
|
+
this.partialChooserActive = false;
|
|
152
|
+
this.updateAttributes({ id: this.partial.id });
|
|
153
|
+
// let rect = this.$el.getBoundingClientRect();
|
|
154
|
+
// console.log(rect);
|
|
155
|
+
// this.absTop = rect.top;
|
|
156
|
+
// this.absLeft = rect.left;
|
|
157
|
+
// this.absWidth = rect.width;
|
|
158
|
+
},
|
|
159
|
+
updateContent(newContent) {
|
|
160
|
+
this.partial.richContent = newContent;
|
|
161
|
+
},
|
|
162
|
+
async savePartial() {
|
|
163
|
+
// force update the content
|
|
164
|
+
this.$refs.editor.onUpdate();
|
|
165
|
+
|
|
166
|
+
let res = await this.partialsApi.update(this.partial);
|
|
167
|
+
this.partial = { ... res.item };
|
|
168
|
+
this.serverPartial = { ... res.item };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<style scoped>
|
|
175
|
+
.wrapper {
|
|
176
|
+
position: relative;
|
|
177
|
+
background-color: #555;
|
|
178
|
+
}
|
|
179
|
+
.wrapper.editable {
|
|
180
|
+
outline: 2px dashed purple;
|
|
181
|
+
padding-top: 30px;
|
|
182
|
+
}
|
|
183
|
+
.wrapper.selected {
|
|
184
|
+
outline-style: solid;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
::v-deep(.editor > .editor-content > .ProseMirror) {
|
|
188
|
+
min-height: 4rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.toolbar {
|
|
192
|
+
display: flex;
|
|
193
|
+
z-index: 11;
|
|
194
|
+
position: absolute;
|
|
195
|
+
top: 0;
|
|
196
|
+
left: 0;
|
|
197
|
+
width: 100%;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.editor-content {
|
|
201
|
+
position: relative;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
p.control {
|
|
205
|
+
margin-bottom: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.my-field {
|
|
209
|
+
display: flex;
|
|
210
|
+
}
|
|
211
|
+
</style>
|
|
212
|
+
|
|
213
|
+
<style>
|
|
214
|
+
.my-field > .control > .button {
|
|
215
|
+
border-radius: 0;
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core'
|
|
2
|
+
import Suggestion from '@tiptap/suggestion'
|
|
3
|
+
import {items} from './suggestion'
|
|
4
|
+
|
|
5
|
+
export default Extension.create({
|
|
6
|
+
name: 'commands',
|
|
7
|
+
|
|
8
|
+
addOptions() {
|
|
9
|
+
return {
|
|
10
|
+
suggestion: {
|
|
11
|
+
char: '/',
|
|
12
|
+
allowSpaces: true,
|
|
13
|
+
command: ({editor, range, props}) => {
|
|
14
|
+
let command = editor.chain().focus().deleteRange(range);
|
|
15
|
+
command = props.command({ editor, range, command });
|
|
16
|
+
command.run();
|
|
17
|
+
},
|
|
18
|
+
items: items,
|
|
19
|
+
render: function() {
|
|
20
|
+
let editorComponent
|
|
21
|
+
return {
|
|
22
|
+
onStart: (props) => {
|
|
23
|
+
editorComponent = props.editor.editorComponent;
|
|
24
|
+
if(!props.clientRect)
|
|
25
|
+
{
|
|
26
|
+
editorComponent.commandsListVisible = false;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let rect = props.clientRect();
|
|
31
|
+
editorComponent.commandsListVisible = true;
|
|
32
|
+
editorComponent.commandsListTop = rect.top;
|
|
33
|
+
editorComponent.commandsListLeft = rect.left;
|
|
34
|
+
editorComponent.commandsListItems = props.items;
|
|
35
|
+
editorComponent.commandsListCommand = props.command;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
onUpdate(props) {
|
|
39
|
+
let rect = props.clientRect();
|
|
40
|
+
editorComponent.commandsListTop = rect.top;
|
|
41
|
+
editorComponent.commandsListLeft = rect.left;
|
|
42
|
+
editorComponent.commandsListItems = props.items;
|
|
43
|
+
editorComponent.commandsListCommand = props.command;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
onKeyDown(props) {
|
|
47
|
+
if (props.event.key === 'Escape') {
|
|
48
|
+
editorComponent.commandsListVisible = false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return editorComponent.$refs['commandsList'].onKeyDown(props);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
onExit() {
|
|
55
|
+
editorComponent.commandsListVisible = false;
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
addProseMirrorPlugins() {
|
|
64
|
+
this.editor.editorComponent = this.options.editorComponent;
|
|
65
|
+
return [
|
|
66
|
+
Suggestion({
|
|
67
|
+
editor: this.editor,
|
|
68
|
+
...this.options.suggestion,
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
},
|
|
72
|
+
})
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
export function items({ query, canConvert }) {
|
|
2
|
+
let items = [
|
|
3
|
+
{
|
|
4
|
+
title: 'Paragraph',
|
|
5
|
+
icon: 'paragraph',
|
|
6
|
+
canConvert: true,
|
|
7
|
+
node: ({editor}) => {
|
|
8
|
+
return editor.schema.node('paragraph');
|
|
9
|
+
},
|
|
10
|
+
command: ({ command }) => {
|
|
11
|
+
return command.setParagraph();
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
title: 'H1',
|
|
16
|
+
icon: 'heading',
|
|
17
|
+
canConvert: true,
|
|
18
|
+
node: ({editor}) => {
|
|
19
|
+
return editor.schema.node('heading', {level: 1});
|
|
20
|
+
},
|
|
21
|
+
command: ({ command }) => {
|
|
22
|
+
return command.setParagraph().setNode('heading', {level: 1});
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'H2',
|
|
27
|
+
icon: 'heading',
|
|
28
|
+
canConvert: true,
|
|
29
|
+
node: ({editor}) => {
|
|
30
|
+
return editor.schema.node('heading', {level: 2});
|
|
31
|
+
},
|
|
32
|
+
command: ({ command }) => {
|
|
33
|
+
return command.setParagraph().setNode('heading', {level: 2});
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: 'H3',
|
|
38
|
+
icon: 'heading',
|
|
39
|
+
canConvert: true,
|
|
40
|
+
node: ({editor}) => {
|
|
41
|
+
return editor.schema.node('heading', {level: 3});
|
|
42
|
+
},
|
|
43
|
+
command: ({ command }) => {
|
|
44
|
+
return command.setParagraph().setNode('heading', {level: 3});
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'Blockquote',
|
|
49
|
+
icon: 'quote-left',
|
|
50
|
+
canConvert: true,
|
|
51
|
+
node: ({editor}) => {
|
|
52
|
+
return editor.schema.node('blockquote', {}, [editor.schema.node('paragraph', {}, [])]);
|
|
53
|
+
},
|
|
54
|
+
command: ({ command }) => {
|
|
55
|
+
return command.setParagraph().setBlockquote();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: 'Ordered List',
|
|
60
|
+
icon: 'list-ol',
|
|
61
|
+
canConvert: true,
|
|
62
|
+
node: ({editor}) => {
|
|
63
|
+
return editor.schema.node('orderedList', {}, [editor.schema.node('listItem', {}, [editor.schema.node('paragraph', {}, [])])]);
|
|
64
|
+
},
|
|
65
|
+
command: ({ command }) => {
|
|
66
|
+
return command.setParagraph().toggleOrderedList();
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: 'Unordered List',
|
|
71
|
+
icon: 'list-ul',
|
|
72
|
+
canConvert: true,
|
|
73
|
+
node: ({editor}) => {
|
|
74
|
+
return editor.schema.node('bulletList', {}, [editor.schema.node('listItem', {}, [editor.schema.node('paragraph', {}, [])])]);
|
|
75
|
+
},
|
|
76
|
+
command: ({ command }) => {
|
|
77
|
+
return command.setParagraph().toggleBulletList();
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: 'Horizontal Rule',
|
|
82
|
+
icon: 'ruler-horizontal',
|
|
83
|
+
canConvert: true,
|
|
84
|
+
node: ({editor}) => {
|
|
85
|
+
return editor.schema.node('horizontalRule');
|
|
86
|
+
},
|
|
87
|
+
command: ({ command }) => {
|
|
88
|
+
return command.setParagraph().setHorizontalRule();
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
// {
|
|
92
|
+
// title: 'Bold',
|
|
93
|
+
// icon: 'bold',
|
|
94
|
+
// command: ({ editor, range }) => {
|
|
95
|
+
// editor
|
|
96
|
+
// .chain()
|
|
97
|
+
// .focus()
|
|
98
|
+
// .deleteRange(range)
|
|
99
|
+
// .setMark('bold')
|
|
100
|
+
// .run()
|
|
101
|
+
// },
|
|
102
|
+
// },
|
|
103
|
+
// {
|
|
104
|
+
// title: 'Italic',
|
|
105
|
+
// icon: 'italic',
|
|
106
|
+
// command: ({ editor, range }) => {
|
|
107
|
+
// editor
|
|
108
|
+
// .chain()
|
|
109
|
+
// .focus()
|
|
110
|
+
// .deleteRange(range)
|
|
111
|
+
// .setMark('italic')
|
|
112
|
+
// .run()
|
|
113
|
+
// },
|
|
114
|
+
// },
|
|
115
|
+
{
|
|
116
|
+
title: 'Media',
|
|
117
|
+
icon: 'photo-video',
|
|
118
|
+
canConvert: false,
|
|
119
|
+
node: ({editor}) => {
|
|
120
|
+
return editor.schema.node('mediaItem');
|
|
121
|
+
},
|
|
122
|
+
command: ({ command }) => {
|
|
123
|
+
return command
|
|
124
|
+
.insertContent({
|
|
125
|
+
type: 'mediaItem'
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
title: 'Raw HTML',
|
|
131
|
+
icon: 'code',
|
|
132
|
+
canConvert: false,
|
|
133
|
+
node: ({editor}) => {
|
|
134
|
+
return editor.schema.node('rawHtml');
|
|
135
|
+
},
|
|
136
|
+
command: ({ command}) => {
|
|
137
|
+
return command
|
|
138
|
+
.insertContent({
|
|
139
|
+
type: 'rawHtml'
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
title: 'Grid Row',
|
|
145
|
+
icon: 'th-large',
|
|
146
|
+
canConvert: false,
|
|
147
|
+
node: ({editor}) => {
|
|
148
|
+
return editor.schema.node('gridRow');
|
|
149
|
+
},
|
|
150
|
+
command: ({ command }) => {
|
|
151
|
+
return command
|
|
152
|
+
.insertContent({
|
|
153
|
+
type: 'gridRow'
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
title: 'Partial',
|
|
159
|
+
icon: 'puzzle-piece',
|
|
160
|
+
canConvert: false,
|
|
161
|
+
node: ({editor}) => {
|
|
162
|
+
return editor.schema.node('partial');
|
|
163
|
+
},
|
|
164
|
+
command: ({ command }) => {
|
|
165
|
+
return command
|
|
166
|
+
.insertContent({
|
|
167
|
+
type: 'partial'
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
title: 'Link to Page',
|
|
173
|
+
icon: 'file-alt',
|
|
174
|
+
canConvert: false,
|
|
175
|
+
node: ({editor}) => {
|
|
176
|
+
return editor.schema.node('objectLink', { type: 'page', id: null, content: null});
|
|
177
|
+
},
|
|
178
|
+
command: ({ command }) => {
|
|
179
|
+
return command
|
|
180
|
+
.insertContent({
|
|
181
|
+
type: 'objectLink',
|
|
182
|
+
attrs: {
|
|
183
|
+
type: 'page', id: null, content: null
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
title: 'Link to Media',
|
|
190
|
+
icon: 'file-image',
|
|
191
|
+
canConvert: false,
|
|
192
|
+
node: ({editor}) => {
|
|
193
|
+
return editor.schema.node('objectLink', { type: 'media', id: null, content: null});
|
|
194
|
+
},
|
|
195
|
+
command: ({ command }) => {
|
|
196
|
+
return command
|
|
197
|
+
.insertContent({
|
|
198
|
+
type: 'objectLink',
|
|
199
|
+
attrs: {
|
|
200
|
+
type: 'media', id: null, content: null
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
items = items.filter(item => item.title.toLowerCase().includes(query.toLowerCase()));
|
|
207
|
+
if(canConvert) {
|
|
208
|
+
items = items.filter(item => item.canConvert);
|
|
209
|
+
}
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
@@ -67,7 +67,7 @@ export default {
|
|
|
67
67
|
},
|
|
68
68
|
data() {
|
|
69
69
|
return {
|
|
70
|
-
mediaDirectoryApi: new MediaDirectoryApi(
|
|
70
|
+
mediaDirectoryApi: new MediaDirectoryApi(),
|
|
71
71
|
newDirectory: null,
|
|
72
72
|
isLoading: false,
|
|
73
73
|
searchQuery: '',
|
|
@@ -92,7 +92,7 @@ export default {
|
|
|
92
92
|
methods: {
|
|
93
93
|
async fetchData() {
|
|
94
94
|
this.isLoading = true;
|
|
95
|
-
let data = await this.mediaDirectoryApi.list(false, 1, null);
|
|
95
|
+
let data = await this.mediaDirectoryApi.list({ inTrash: false, page: 1, q: null });
|
|
96
96
|
this.directoriesList = data.items;
|
|
97
97
|
this.directoriesList.sort((a, b) => {
|
|
98
98
|
return getDirectoryPathString(a).localeCompare(getDirectoryPathString(b))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<b-modal :active
|
|
2
|
+
<b-modal :active="active" trap-focus has-modal-card aria-role="dialog" aria-modal auto-focus width="80%" class="media-insert-modal" @update:active="updateActive">
|
|
3
3
|
<div class="modal-card">
|
|
4
4
|
<header class="modal-card-head">
|
|
5
5
|
<slot name="title"><p class="modal-card-title">Choose an item to insert</p></slot>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</section>
|
|
10
10
|
<footer class="modal-card-foot is-flex">
|
|
11
11
|
<div class="is-flex-grow-1"></div>
|
|
12
|
-
<b-button @click="emitClose">
|
|
12
|
+
<b-button @click="emitClose">{{ closeVerb }}</b-button>
|
|
13
13
|
<b-button :disabled="selectedFiles.length === 0 || (!multiselectAllowed && selectedFiles.length > 1)" type="is-primary" @click="doInsert">
|
|
14
14
|
<span v-if="selectedFiles.length === 0">{{ actionVerb }}</span>
|
|
15
15
|
<span v-else-if="selectedFiles.length === 1">{{actionVerb }} item</span>
|
|
@@ -36,6 +36,10 @@ export default {
|
|
|
36
36
|
actionVerb: {
|
|
37
37
|
type: String,
|
|
38
38
|
default: 'Insert'
|
|
39
|
+
},
|
|
40
|
+
closeVerb: {
|
|
41
|
+
type: String,
|
|
42
|
+
default: 'Close'
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
data() {
|
|
@@ -62,6 +66,11 @@ export default {
|
|
|
62
66
|
},
|
|
63
67
|
doInsert() {
|
|
64
68
|
this.$emit('select', this.selectedFiles);
|
|
69
|
+
},
|
|
70
|
+
updateActive(newVal) {
|
|
71
|
+
if(!newVal) {
|
|
72
|
+
this.emitClose();
|
|
73
|
+
}
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="media-icon-container">
|
|
3
|
-
<img v-if="item.type === TYPE_IMAGE && !loadingError" :src="'
|
|
3
|
+
<img v-if="item.type === TYPE_IMAGE && !loadingError" :src="getApiHost() + 'content/media/' + item.filename" :alt="item.caption ? item.caption : item.name" @error="loadingFailed">
|
|
4
4
|
<b-tooltip v-else-if="item.type === TYPE_IMAGE" :label="missingMessage" type="is-dark" position="is-bottom" multilined>
|
|
5
5
|
<b-icon type="is-danger" icon="file-image" size="is-large" class="media-icon"></b-icon>
|
|
6
6
|
</b-tooltip>
|
|
7
|
-
<img v-if="item.type === TYPE_DOCUMENT && !loadingError" :src="'
|
|
7
|
+
<img v-if="item.type === TYPE_DOCUMENT && !loadingError" :src="getApiRoot() + 'media/' + item.id + '/preview'" alt="PDF preview" @error="loadingFailed">
|
|
8
8
|
<b-tooltip v-else-if="item.type === TYPE_DOCUMENT" :label="missingMessage" type="is-dark" position="is-bottom" multilined>
|
|
9
9
|
<b-icon type="is-danger" icon="file-pdf" size="is-large" class="media-icon"></b-icon>
|
|
10
10
|
</b-tooltip>
|
|
11
11
|
<b-icon v-else-if="item.type === TYPE_AUDIO" icon="music" size="is-large" class="media-icon"></b-icon>
|
|
12
|
+
<div class="media-icon-toolbar"><slot></slot></div>
|
|
12
13
|
</div>
|
|
13
14
|
</template>
|
|
14
15
|
|
|
15
16
|
<script>
|
|
16
17
|
import MediaApi from "../../MediaApi";
|
|
18
|
+
import {getApiRoot} from "../../CrudApi.js";
|
|
19
|
+
import {getApiHost} from "../../api.js";
|
|
17
20
|
|
|
18
21
|
export default {
|
|
19
22
|
name: "MediaItemPreview",
|
|
@@ -26,13 +29,19 @@ export default {
|
|
|
26
29
|
},
|
|
27
30
|
data() {
|
|
28
31
|
return {
|
|
32
|
+
getApiHost: getApiHost,
|
|
33
|
+
getApiRoot: getApiRoot,
|
|
29
34
|
TYPE_IMAGE: MediaApi.TYPE_IMAGE,
|
|
30
35
|
TYPE_AUDIO: MediaApi.TYPE_AUDIO,
|
|
31
36
|
TYPE_DOCUMENT: MediaApi.TYPE_DOCUMENT,
|
|
32
37
|
loadingError: false
|
|
33
38
|
}
|
|
34
39
|
},
|
|
40
|
+
watch: { 'item': 'resetLoadingFailed' },
|
|
35
41
|
methods: {
|
|
42
|
+
resetLoadingFailed() {
|
|
43
|
+
this.loadingError = false;
|
|
44
|
+
},
|
|
36
45
|
loadingFailed() {
|
|
37
46
|
this.loadingError = true;
|
|
38
47
|
}
|
|
@@ -42,4 +51,11 @@ export default {
|
|
|
42
51
|
|
|
43
52
|
<style scoped lang="scss">
|
|
44
53
|
@import './media.scss';
|
|
54
|
+
|
|
55
|
+
.media-icon-toolbar {
|
|
56
|
+
z-index: 10;
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 1rem;
|
|
59
|
+
right: 1rem;
|
|
60
|
+
}
|
|
45
61
|
</style>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
2
|
<div class="full-height full-height-container media-container">
|
|
4
3
|
<div class="top-bar">
|
|
5
4
|
<div v-if="!paginatedItems.loading && !inTrash && !searchQuery" class="breadcrumb">
|
|
@@ -20,7 +19,7 @@
|
|
|
20
19
|
<b-button outlined rounded icon-left="arrow-left" class="action-bar-pad" @click="navigateTo({ currentPath: '' })">All Items</b-button>
|
|
21
20
|
<div class="title action-bar-pad">Search results for "{{ searchQuery }}"</div>
|
|
22
21
|
</div>
|
|
23
|
-
<ul v-else>
|
|
22
|
+
<ul v-else class="breadcrumb">
|
|
24
23
|
<li><b-skeleton :animated="true" size="is-large" :width="100"></b-skeleton></li>
|
|
25
24
|
</ul>
|
|
26
25
|
|
|
@@ -140,8 +139,8 @@ export default {
|
|
|
140
139
|
isCreateDirectoryModalActive: false,
|
|
141
140
|
newDirectoryName: '',
|
|
142
141
|
searchDebounce: null,
|
|
143
|
-
mediaApi: new MediaApi(
|
|
144
|
-
mediaDirectoryApi: new MediaDirectoryApi(
|
|
142
|
+
mediaApi: new MediaApi(),
|
|
143
|
+
mediaDirectoryApi: new MediaDirectoryApi(),
|
|
145
144
|
getDirectoryBreadcrumbItems: getDirectoryBreadcrumbItems
|
|
146
145
|
}
|
|
147
146
|
},
|
|
@@ -174,7 +173,7 @@ export default {
|
|
|
174
173
|
this.paginatedItems.files = []
|
|
175
174
|
this.paginatedItems.directories = [];
|
|
176
175
|
|
|
177
|
-
let data = await this.mediaApi.list(this.inTrash, this.searchQuery, this.paginatedItems.currentPage, this.searchQuery ? '' : this.currentPath);
|
|
176
|
+
let data = await this.mediaApi.list({ inTrash: this.inTrash, q: this.searchQuery, page: this.paginatedItems.currentPage, path: this.searchQuery ? '' : this.currentPath });
|
|
178
177
|
|
|
179
178
|
this.paginatedItems.currentDirectory = data.currentDirectory;
|
|
180
179
|
this.paginatedItems.directories = data.directories.map(dir => { dir.selected = false; return dir; });
|