@live-change/wysiwyg-frontend 0.2.20 → 0.2.21
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/front/src/App.vue +6 -0
- package/front/src/components/CodeBlockNode.js +80 -0
- package/front/src/components/CodeBlockView.js +139 -0
- package/front/src/components/ComponentComponent.vue +38 -3
- package/front/src/components/ComponentNode.js +3 -8
- package/front/src/components/ContentView.js +1 -0
- package/front/src/components/DocumentEditor.vue +9 -5
- package/front/src/components/EditorMenu.vue +32 -31
- package/front/src/components/ImageComponent.vue +1 -1
- package/front/src/components/SlotNode.js +54 -0
- package/front/src/components/TemplatesMenu.vue +68 -0
- package/front/src/components/components.js +8 -5
- package/front/src/components/contentConfig.js +22 -2
- package/front/src/components/contentConfigExtensions.js +9 -3
- package/front/src/components/templates.js +70 -0
- package/package.json +7 -2
- package/server/page.documentType.js +25 -4
- package/front/src/components/ComponentsMenu.vue +0 -108
package/front/src/App.vue
CHANGED
|
@@ -35,4 +35,10 @@
|
|
|
35
35
|
api.validators.email = emailValidator
|
|
36
36
|
api.validators.password = passwordValidator
|
|
37
37
|
|
|
38
|
+
import { defaultHighlightStyle } from "@codemirror/language"
|
|
39
|
+
import { StyleModule } from "style-mod"
|
|
40
|
+
if(typeof window != 'undefined') {
|
|
41
|
+
StyleModule.mount(window.document, defaultHighlightStyle.module)
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
</script>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core'
|
|
2
|
+
import CodeBlockView from "./CodeBlockView.js"
|
|
3
|
+
|
|
4
|
+
function arrowHandler(dir) {
|
|
5
|
+
return (state, dispatch, view) => {
|
|
6
|
+
if (state.selection.empty && view.endOfTextblock(dir)) {
|
|
7
|
+
let side = dir == "left" || dir == "up" ? -1 : 1
|
|
8
|
+
let $head = state.selection.$head
|
|
9
|
+
let nextPos = Selection.near(
|
|
10
|
+
state.doc.resolve(side > 0 ? $head.after() : $head.before()), side)
|
|
11
|
+
if (nextPos.$head && nextPos.$head.parent.type.name == "code_block") {
|
|
12
|
+
dispatch(state.tr.setSelection(nextPos))
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default Node.create({
|
|
21
|
+
name: 'codeBlock',
|
|
22
|
+
group: 'block',
|
|
23
|
+
atom: false,
|
|
24
|
+
inline: false,
|
|
25
|
+
selectable: true,
|
|
26
|
+
draggable: true,
|
|
27
|
+
content: 'text*',
|
|
28
|
+
marks: '',
|
|
29
|
+
|
|
30
|
+
addAttributes() {
|
|
31
|
+
return {
|
|
32
|
+
language: {
|
|
33
|
+
rendered: false,
|
|
34
|
+
default: 'text'
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/*parseHTML() {
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
tag: this.options.allowBase64
|
|
43
|
+
? 'img[src]'
|
|
44
|
+
: 'img[src]:not([src^="data:"])',
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
},*/
|
|
48
|
+
|
|
49
|
+
renderHTML({ }) {
|
|
50
|
+
return ['img', {}]
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
addNodeView() {
|
|
54
|
+
return (args) => {
|
|
55
|
+
const { editor, node, getPos } = args
|
|
56
|
+
return new CodeBlockView(node, editor, getPos)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
addCommands() {
|
|
61
|
+
return {
|
|
62
|
+
setCodeBlock: attributes => ({ commands }) => {
|
|
63
|
+
console.log("SET CODE BLOCK", attributes)
|
|
64
|
+
return commands.setNode(this.name, attributes)
|
|
65
|
+
},
|
|
66
|
+
toggleCodeBlock: attributes => ({ commands }) => {
|
|
67
|
+
return commands.toggleNode(this.name, 'paragraph', attributes)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
addKeyboardShortcuts() {
|
|
73
|
+
return {
|
|
74
|
+
ArrowLeft: arrowHandler("left"),
|
|
75
|
+
ArrowRight: arrowHandler("right"),
|
|
76
|
+
ArrowUp: arrowHandler("up"),
|
|
77
|
+
ArrowDown: arrowHandler("down")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { TextSelection } from 'prosemirror-state'
|
|
2
|
+
import {
|
|
3
|
+
EditorView, keymap as cmKeymap, drawSelection
|
|
4
|
+
} from "@codemirror/view"
|
|
5
|
+
import {javascript} from "@codemirror/lang-javascript"
|
|
6
|
+
import {defaultKeymap} from "@codemirror/commands"
|
|
7
|
+
import {syntaxHighlighting, defaultHighlightStyle} from "@codemirror/language"
|
|
8
|
+
|
|
9
|
+
import {exitCode} from "prosemirror-commands"
|
|
10
|
+
import {undo, redo} from "prosemirror-history"
|
|
11
|
+
|
|
12
|
+
class CodeBlockView {
|
|
13
|
+
constructor(node, editor, getPos) {
|
|
14
|
+
// Store for later
|
|
15
|
+
this.node = node
|
|
16
|
+
this.view = editor.view
|
|
17
|
+
this.schema = editor.schema
|
|
18
|
+
this.getPos = getPos
|
|
19
|
+
|
|
20
|
+
this.language = node.attrs.language
|
|
21
|
+
|
|
22
|
+
// Select language plugin
|
|
23
|
+
let languagePlugins = []
|
|
24
|
+
switch(this.language) {
|
|
25
|
+
case 'javascript':
|
|
26
|
+
languagePlugins.push(javascript())
|
|
27
|
+
break
|
|
28
|
+
default:
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create a CodeMirror instance
|
|
33
|
+
this.cm = new EditorView({
|
|
34
|
+
doc: this.node.textContent,
|
|
35
|
+
extensions: [
|
|
36
|
+
cmKeymap.of([
|
|
37
|
+
...this.codeMirrorKeymap(),
|
|
38
|
+
...defaultKeymap
|
|
39
|
+
]),
|
|
40
|
+
drawSelection(),
|
|
41
|
+
syntaxHighlighting(defaultHighlightStyle),
|
|
42
|
+
...languagePlugins,
|
|
43
|
+
EditorView.updateListener.of(update => this.forwardUpdate(update))
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
// The editor's outer node is our DOM representation
|
|
47
|
+
this.dom = this.cm.dom
|
|
48
|
+
|
|
49
|
+
// This flag is used to avoid an update loop between the outer and
|
|
50
|
+
// inner editor
|
|
51
|
+
this.updating = false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
forwardUpdate(update) {
|
|
55
|
+
if (this.updating || !this.cm.hasFocus) return
|
|
56
|
+
let offset = this.getPos() + 1, {main} = update.state.selection
|
|
57
|
+
let selection = TextSelection.create(this.view.state.doc,
|
|
58
|
+
offset + main.from, offset + main.to)
|
|
59
|
+
if (update.docChanged || !this.view.state.selection.eq(selection)) {
|
|
60
|
+
let tr = this.view.state.tr.setSelection(selection)
|
|
61
|
+
update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
62
|
+
if (text.length)
|
|
63
|
+
tr.replaceWith(offset + fromA, offset + toA,
|
|
64
|
+
this.schema.text(text.toString()))
|
|
65
|
+
else
|
|
66
|
+
tr.delete(offset + fromA, offset + toA)
|
|
67
|
+
offset += (toB - fromB) - (toA - fromA)
|
|
68
|
+
})
|
|
69
|
+
this.view.dispatch(tr)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
codeMirrorKeymap() {
|
|
74
|
+
let view = this.view
|
|
75
|
+
return [
|
|
76
|
+
{key: "ArrowUp", run: () => this.maybeEscape("line", -1)},
|
|
77
|
+
{key: "ArrowLeft", run: () => this.maybeEscape("char", -1)},
|
|
78
|
+
{key: "ArrowDown", run: () => this.maybeEscape("line", 1)},
|
|
79
|
+
{key: "ArrowRight", run: () => this.maybeEscape("char", 1)},
|
|
80
|
+
{key: "Ctrl-Enter", run: () => {
|
|
81
|
+
if (!exitCode(view.state, view.dispatch)) return false
|
|
82
|
+
view.focus()
|
|
83
|
+
return true
|
|
84
|
+
}},
|
|
85
|
+
{key: "Ctrl-z", mac: "Cmd-z",
|
|
86
|
+
run: () => undo(view.state, view.dispatch)},
|
|
87
|
+
{key: "Shift-Ctrl-z", mac: "Shift-Cmd-z",
|
|
88
|
+
run: () => redo(view.state, view.dispatch)},
|
|
89
|
+
{key: "Ctrl-y", mac: "Cmd-y",
|
|
90
|
+
run: () => redo(view.state, view.dispatch)}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
maybeEscape(unit, dir) {
|
|
95
|
+
let {state} = this.cm, {main} = state.selection
|
|
96
|
+
if (!main.empty) return false
|
|
97
|
+
if (unit == "line") main = state.doc.lineAt(main.head)
|
|
98
|
+
if (dir < 0 ? main.from > 0 : main.to < state.doc.length) return false
|
|
99
|
+
let targetPos = this.getPos() + (dir < 0 ? 0 : this.node.nodeSize)
|
|
100
|
+
let selection = Selection.near(this.view.state.doc.resolve(targetPos), dir)
|
|
101
|
+
let tr = this.view.state.tr.setSelection(selection).scrollIntoView()
|
|
102
|
+
this.view.dispatch(tr)
|
|
103
|
+
this.view.focus()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
update(node) {
|
|
107
|
+
if (node.type != this.node.type) return false
|
|
108
|
+
if (node.attrs.language != this.language) return false
|
|
109
|
+
if (this.updating) return true
|
|
110
|
+
let newText = node.textContent, curText = this.cm.state.doc.toString()
|
|
111
|
+
if (newText != curText) {
|
|
112
|
+
let start = 0, curEnd = curText.length, newEnd = newText.length
|
|
113
|
+
while (start < curEnd &&
|
|
114
|
+
curText.charCodeAt(start) == newText.charCodeAt(start)) {
|
|
115
|
+
++start
|
|
116
|
+
}
|
|
117
|
+
while (curEnd > start && newEnd > start &&
|
|
118
|
+
curText.charCodeAt(curEnd - 1) == newText.charCodeAt(newEnd - 1)) {
|
|
119
|
+
curEnd--
|
|
120
|
+
newEnd--
|
|
121
|
+
}
|
|
122
|
+
this.updating = true
|
|
123
|
+
this.cm.dispatch({
|
|
124
|
+
changes: {
|
|
125
|
+
from: start, to: curEnd,
|
|
126
|
+
insert: newText.slice(start, newEnd)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
this.updating = false
|
|
130
|
+
}
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
selectNode() { this.cm.focus() }
|
|
135
|
+
stopEvent() { return true }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
export default CodeBlockView
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<node-view-wrapper>
|
|
3
|
-
<ComponentEditor v-bind="{
|
|
4
|
-
|
|
3
|
+
<ComponentEditor v-bind="{
|
|
4
|
+
...attrs, class: ['relative', editorClass]
|
|
5
|
+
}">
|
|
6
|
+
<node-view-content :class="[editorContentClass]"
|
|
7
|
+
:style="style" />
|
|
5
8
|
<div class="absolute top-0 right-0 pr-4 max-h-0 align-items-center z-5 edit-buttons ">
|
|
6
9
|
<Button icon="pi pi-eye"
|
|
7
10
|
:class="[
|
|
@@ -43,7 +46,9 @@
|
|
|
43
46
|
const is = computed(() => props.node.attrs.is)
|
|
44
47
|
const attrs = computed(() => props.node.attrs.attrs)
|
|
45
48
|
|
|
46
|
-
const
|
|
49
|
+
const component = computed(() => components[props.node.attrs.is])
|
|
50
|
+
|
|
51
|
+
const ComponentEditor = computed(() => component.value.editor)
|
|
47
52
|
|
|
48
53
|
const uid = getCurrentInstance().uid
|
|
49
54
|
|
|
@@ -68,6 +73,23 @@
|
|
|
68
73
|
: null
|
|
69
74
|
})
|
|
70
75
|
|
|
76
|
+
const editorClass = computed(() => [
|
|
77
|
+
component.value.editorClass
|
|
78
|
+
? component.value.editorClass(attrs.value)
|
|
79
|
+
: attrs.value.class,
|
|
80
|
+
{ 'selected-component-editor': selectedEditor.value },
|
|
81
|
+
{ 'selected-component-editor-style': selectedEditor.value == 'style' },
|
|
82
|
+
{ 'selected-component-editor-settings': selectedEditor.value == 'settings' }
|
|
83
|
+
])
|
|
84
|
+
const editorContentClass = computed(() => [
|
|
85
|
+
component.value.editorContentClass
|
|
86
|
+
? component.value.editorContentClass(attrs.value)
|
|
87
|
+
: 'content',
|
|
88
|
+
{ 'selected-component-editor-content': selectedEditor.value },
|
|
89
|
+
{ 'selected-component-editor-content-style': selectedEditor.value == 'style' },
|
|
90
|
+
{ 'selected-component-editor-content-settings': selectedEditor.value == 'settings' }
|
|
91
|
+
])
|
|
92
|
+
|
|
71
93
|
</script>
|
|
72
94
|
|
|
73
95
|
<style lang="scss">
|
|
@@ -77,5 +99,18 @@
|
|
|
77
99
|
.show-edit-buttons .edit-buttons {
|
|
78
100
|
display: flex;
|
|
79
101
|
}
|
|
102
|
+
.selected-component-editor-content-settings {
|
|
103
|
+
& > [data-slot] {
|
|
104
|
+
&::before {
|
|
105
|
+
content: 'slot: ' attr(data-slot) ;
|
|
106
|
+
color: orangered;
|
|
107
|
+
border: orangered 1px solid;
|
|
108
|
+
font-size: 0.8em;
|
|
109
|
+
padding: 2px;
|
|
110
|
+
margin: 0;
|
|
111
|
+
};
|
|
112
|
+
border: 1px dashed orangered;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
80
115
|
|
|
81
116
|
</style>
|
|
@@ -9,7 +9,7 @@ export default Node.create({
|
|
|
9
9
|
inline: false,
|
|
10
10
|
selectable: true,
|
|
11
11
|
draggable: true,
|
|
12
|
-
content: 'block*',
|
|
12
|
+
content: '(block | slot)*',
|
|
13
13
|
marks: '',
|
|
14
14
|
|
|
15
15
|
addAttributes() {
|
|
@@ -54,7 +54,7 @@ export default Node.create({
|
|
|
54
54
|
}
|
|
55
55
|
htmlAttrs['data-is'] = this.options.is
|
|
56
56
|
return ['component', mergeAttributes(HTMLAttributes, htmlAttrs)]*/
|
|
57
|
-
return ['component', HTMLAttributes]
|
|
57
|
+
return ['component', HTMLAttributes, 0]
|
|
58
58
|
},
|
|
59
59
|
|
|
60
60
|
addNodeView() {
|
|
@@ -63,12 +63,7 @@ export default Node.create({
|
|
|
63
63
|
|
|
64
64
|
addCommands() {
|
|
65
65
|
return {
|
|
66
|
-
|
|
67
|
-
return commands.insertContent({
|
|
68
|
-
type: this.name,
|
|
69
|
-
attrs: options
|
|
70
|
-
})
|
|
71
|
-
}
|
|
66
|
+
|
|
72
67
|
}
|
|
73
68
|
}
|
|
74
69
|
})
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
|
38
38
|
import { History } from '@tiptap/extension-history'
|
|
39
|
+
import Gapcursor from '@tiptap/extension-gapcursor'
|
|
39
40
|
import {
|
|
40
41
|
ref, computed, watch, provide, defineEmits, defineProps, getCurrentInstance, onUnmounted, inject, onMounted,
|
|
41
42
|
shallowRef
|
|
@@ -122,7 +123,7 @@
|
|
|
122
123
|
const currentVersion = getVersion(state)
|
|
123
124
|
console.log("COLLAB VERSION", currentVersion)
|
|
124
125
|
let { steps, clientIDs } = authority.stepsSince(currentVersion, state.schema)
|
|
125
|
-
console.
|
|
126
|
+
console.log("RECEIVE STEPS", steps, clientIDs, clientID)
|
|
126
127
|
const transaction = receiveTransaction(state, steps, clientIDs)
|
|
127
128
|
view.dispatch(transaction)
|
|
128
129
|
version.value = authority.remoteVersion
|
|
@@ -131,7 +132,7 @@
|
|
|
131
132
|
const state = editor.value.reactiveState.value
|
|
132
133
|
const sendable = sendableSteps(state)
|
|
133
134
|
if (sendable) {
|
|
134
|
-
console.
|
|
135
|
+
console.log("SEND STEPS", sendable.steps, sendable.version)
|
|
135
136
|
authority.receiveSteps(sendable.version, sendable.steps, sendable.clientID)
|
|
136
137
|
}
|
|
137
138
|
})
|
|
@@ -146,6 +147,7 @@
|
|
|
146
147
|
content: documentData.content,
|
|
147
148
|
extensions: [
|
|
148
149
|
...extensions,
|
|
150
|
+
Gapcursor,
|
|
149
151
|
ProseMirrorCollab.configure({
|
|
150
152
|
version: documentData.version,
|
|
151
153
|
clientID
|
|
@@ -182,7 +184,7 @@
|
|
|
182
184
|
const state = editor.value.reactiveState.value
|
|
183
185
|
const sendable = sendableSteps(state)
|
|
184
186
|
if (sendable) {
|
|
185
|
-
console.
|
|
187
|
+
console.log("SEND STEPS", sendable.steps, sendable.version)
|
|
186
188
|
authority.receiveSteps(sendable.version, sendable.steps, sendable.clientID)
|
|
187
189
|
}
|
|
188
190
|
}
|
|
@@ -216,6 +218,8 @@
|
|
|
216
218
|
|
|
217
219
|
</script>
|
|
218
220
|
|
|
219
|
-
<style
|
|
220
|
-
|
|
221
|
+
<style>
|
|
222
|
+
.cm-line, .cm-content {
|
|
223
|
+
padding: 0 !important;
|
|
224
|
+
}
|
|
221
225
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="sticky" style="top: 90px; z-index:
|
|
2
|
+
<div class="sticky" style="top: 90px; z-index: 10">
|
|
3
3
|
<slot name="before" />
|
|
4
4
|
<div class="surface-card p-1 shadow-2 border-round flex flex-row flex-wrap">
|
|
5
5
|
|
|
@@ -55,8 +55,13 @@
|
|
|
55
55
|
:class="{ 'p-button-outlined': !editor.isActive('blockquote') }"
|
|
56
56
|
@click="editor.chain().focus().toggleBlockquote().run()" />
|
|
57
57
|
<Button v-if="config.nodes.codeBlock" icon="fa-solid fa-code" class="p-button-sm"
|
|
58
|
-
:class="{ 'p-button-outlined': !editor.isActive('
|
|
58
|
+
:class="{ 'p-button-outlined': !editor.isActive('codeBlock') }"
|
|
59
59
|
@click="editor.chain().focus().toggleCodeBlock().run()" />
|
|
60
|
+
<Dropdown v-if="editor.isActive('codeBlock')" class="editor-menu-dropdown"
|
|
61
|
+
:options="languages"
|
|
62
|
+
:modelValue="editor.getAttributes('codeBlock').language"
|
|
63
|
+
:optionLabel="option => option[0].toUpperCase()+option.slice(1)"
|
|
64
|
+
@update:modelValue="selected => editor.chain().focus().setCodeBlock({ language: selected }).run()" />
|
|
60
65
|
</div>
|
|
61
66
|
|
|
62
67
|
<div class="p-buttonset mr-1 mb-1">
|
|
@@ -76,9 +81,9 @@
|
|
|
76
81
|
</FileInput>
|
|
77
82
|
<!-- </div>-->
|
|
78
83
|
|
|
79
|
-
<Button v-if="config.nodes.component" label="
|
|
84
|
+
<Button v-if="config.nodes.component" label="Insert"
|
|
80
85
|
class="p-button-sm p-button-primary p-button-outlined inline-block mr-1 mb-1"
|
|
81
|
-
@click="
|
|
86
|
+
@click="openTemplateMenu" />
|
|
82
87
|
|
|
83
88
|
<span v-if="saveState"
|
|
84
89
|
class="p-button p-component p-button-sm p-button-outlined p-button-secondary cursor-auto
|
|
@@ -99,12 +104,13 @@
|
|
|
99
104
|
|
|
100
105
|
</div>
|
|
101
106
|
<slot name="after" />
|
|
102
|
-
<OverlayPanel ref="
|
|
103
|
-
<
|
|
107
|
+
<OverlayPanel ref="insertTemplateOverlay" style="width: 450px" :breakpoints="{'960px': '75vw'}">
|
|
108
|
+
<TemplatesMenu :config="config" @selected="t => handleTemplateSelected(t)" />
|
|
104
109
|
</OverlayPanel>
|
|
110
|
+
|
|
105
111
|
<!-- <div class="surface-card p-1 shadow-2" style="width: 450px">
|
|
106
112
|
<h3>components:</h3>
|
|
107
|
-
<
|
|
113
|
+
<TemplatesMenu :config="config" />
|
|
108
114
|
</div>-->
|
|
109
115
|
</div>
|
|
110
116
|
</template>
|
|
@@ -113,9 +119,10 @@
|
|
|
113
119
|
import Button from "primevue/button"
|
|
114
120
|
import ProgressSpinner from "primevue/progressspinner"
|
|
115
121
|
import OverlayPanel from 'primevue/overlaypanel'
|
|
122
|
+
import Dropdown from "primevue/dropdown"
|
|
116
123
|
|
|
117
|
-
import
|
|
118
|
-
import { inject, getCurrentInstance, ref } from "vue"
|
|
124
|
+
import TemplatesMenu from "./TemplatesMenu.vue"
|
|
125
|
+
import { inject, getCurrentInstance, ref, computed } from "vue"
|
|
119
126
|
import { toRefs } from "@vueuse/core"
|
|
120
127
|
import { uploadImage } from "@live-change/image-frontend"
|
|
121
128
|
import { FileInput } from "@live-change/upload-frontend"
|
|
@@ -135,15 +142,13 @@
|
|
|
135
142
|
saveState: {
|
|
136
143
|
type: String,
|
|
137
144
|
default: null
|
|
138
|
-
},
|
|
139
|
-
availableComponents: {
|
|
140
|
-
type: Array,
|
|
141
|
-
default: () => []
|
|
142
145
|
}
|
|
143
146
|
})
|
|
144
147
|
|
|
145
148
|
const { editor, config, saveState } = toRefs(props)
|
|
146
149
|
|
|
150
|
+
const languages = computed(() => config.languages ?? ['plaintext', 'javascript'])
|
|
151
|
+
|
|
147
152
|
async function handleImageUpload(file) {
|
|
148
153
|
const upload = uploadImage('test', { file },
|
|
149
154
|
{ preparedPreview: true, appContext, generateId: true })
|
|
@@ -155,32 +160,28 @@
|
|
|
155
160
|
await upload.upload()
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
const
|
|
159
|
-
function
|
|
160
|
-
|
|
163
|
+
const insertTemplateOverlay = ref()
|
|
164
|
+
function openTemplateMenu(event) {
|
|
165
|
+
insertTemplateOverlay.value.show(event)
|
|
161
166
|
}
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
editor.value.chain().focus().insertContent(
|
|
165
|
-
type: 'component',
|
|
166
|
-
attrs: {
|
|
167
|
-
is: name,
|
|
168
|
-
attrs: component.initialAttrs
|
|
169
|
-
},
|
|
170
|
-
content: [
|
|
171
|
-
{ type: 'paragraph', content: [
|
|
172
|
-
{ type: 'text', text: 'test' }
|
|
173
|
-
] }
|
|
174
|
-
]
|
|
175
|
-
}).run()
|
|
167
|
+
function handleTemplateSelected(template) {
|
|
168
|
+
insertTemplateOverlay.value.hide()
|
|
169
|
+
editor.value.chain().focus().insertContent(template.content()).run()
|
|
176
170
|
//editor.value.chain().focus().setNode('component', { is: component }).run()
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
if(typeof window != 'undefined') window.editor = editor.value
|
|
180
174
|
</script>
|
|
181
175
|
|
|
182
|
-
<style scoped>
|
|
176
|
+
<style scoped lang="scss">
|
|
183
177
|
.p-buttonset .p-button:not(:last-child) { /* buttonset bug fix */
|
|
184
178
|
border-right: 0 none !important;
|
|
185
179
|
}
|
|
186
180
|
</style>
|
|
181
|
+
<style lang="scss">
|
|
182
|
+
.editor-menu-dropdown {
|
|
183
|
+
.p-dropdown-label {
|
|
184
|
+
padding: 0.345em 0.5em !important;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
+
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
3
|
+
import ComponentComponent from "./ComponentComponent.vue"
|
|
4
|
+
|
|
5
|
+
export default Node.create({
|
|
6
|
+
name: 'slot',
|
|
7
|
+
group: 'slot',
|
|
8
|
+
//atom: true,
|
|
9
|
+
inline: false,
|
|
10
|
+
selectable: false,
|
|
11
|
+
draggable: false,
|
|
12
|
+
content: 'block*',
|
|
13
|
+
marks: '',
|
|
14
|
+
|
|
15
|
+
addAttributes() {
|
|
16
|
+
return {
|
|
17
|
+
name: {
|
|
18
|
+
default: 'default',
|
|
19
|
+
renderHTML: attributes => {
|
|
20
|
+
return {
|
|
21
|
+
'data-name': attributes.name
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
tag: {
|
|
26
|
+
default: 'div',
|
|
27
|
+
renderHTML: attributes => {
|
|
28
|
+
return {
|
|
29
|
+
'data-tag': attributes.tag
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
attrs: {
|
|
34
|
+
default: {},
|
|
35
|
+
renderHTML: attributes => {
|
|
36
|
+
const htmlAttrs = {}
|
|
37
|
+
const attrs = attributes.attrs
|
|
38
|
+
for(let key in this.options) {
|
|
39
|
+
htmlAttrs[`data-attr-${key}`] = attrs[key]
|
|
40
|
+
}
|
|
41
|
+
return htmlAttrs
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
48
|
+
const slotAttributes = {
|
|
49
|
+
...node.attrs.attrs,
|
|
50
|
+
'data-slot': node.attrs.name
|
|
51
|
+
}
|
|
52
|
+
return [node.attrs.tag, mergeAttributes(HTMLAttributes, slotAttributes), 0]
|
|
53
|
+
}
|
|
54
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="px-2 pb-2">
|
|
4
|
+
<span class="p-input-icon-left w-full flex">
|
|
5
|
+
<i class="pi pi-search flex-shrink-0" />
|
|
6
|
+
<InputText type="text" v-model="searchQuery" placeholder="Search" class="flex-grow-1" v-focus />
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="overflow-auto" style="max-height: 75vh">
|
|
10
|
+
<!-- <div v-for="i in [1,2,3,4,5,6,7,8,9,10]">-->
|
|
11
|
+
<div v-for="(template, index) in searchResults" :key="template.name"
|
|
12
|
+
class="flex flex-wrap relative cursor-pointer hover:surface-100"
|
|
13
|
+
@click="() => emit('selected', template)" >
|
|
14
|
+
<div class="w-3 ml-2 mr-2 my-1" style="aspect-ratio: 1/1">
|
|
15
|
+
<div class="pointer-events-none w-6 absolute p-1"
|
|
16
|
+
style="aspect-ratio: 1/1; transform: scale(0.5); transform-origin: 0 0;">
|
|
17
|
+
<ContentView :config="config"
|
|
18
|
+
:content="{ type: 'doc', content: template.content({ preview: true }) }"
|
|
19
|
+
style="aspect-ratio: 1/1" />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="ml-2">
|
|
23
|
+
<h3>{{ template.name[0].toUpperCase() + template.name.slice(1) }}</h3>
|
|
24
|
+
<p>{{ template.description }}</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<!-- </div>-->
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
import InputText from "primevue/inputtext"
|
|
34
|
+
|
|
35
|
+
import ContentView from "./ContentView.js"
|
|
36
|
+
import templates from "./templates.js"
|
|
37
|
+
|
|
38
|
+
import { ref, computed } from 'vue'
|
|
39
|
+
|
|
40
|
+
const props = defineProps({
|
|
41
|
+
config: {
|
|
42
|
+
type: Object,
|
|
43
|
+
required: true
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const { config } = props
|
|
48
|
+
|
|
49
|
+
const emit = defineEmits(['selected'])
|
|
50
|
+
|
|
51
|
+
const searchQuery = ref('')
|
|
52
|
+
|
|
53
|
+
const searchResults = computed(() => {
|
|
54
|
+
const query = searchQuery.value.toLowerCase()
|
|
55
|
+
const byName = templates.filter(({ name }) => {
|
|
56
|
+
return name.toLowerCase().includes(query)
|
|
57
|
+
})
|
|
58
|
+
const byDescription = templates.filter(({ description }) => {
|
|
59
|
+
return (description || '').toLowerCase().includes(query)
|
|
60
|
+
}).filter(({ name }) => !byName.find(found => name == found.name))
|
|
61
|
+
return [...byName, ...byDescription]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
|
|
68
|
+
</style>
|
|
@@ -2,14 +2,17 @@ import { h } from 'vue'
|
|
|
2
2
|
|
|
3
3
|
const components = {
|
|
4
4
|
card: {
|
|
5
|
-
description: 'PrimeVUE card',
|
|
6
|
-
previewContent: true,
|
|
7
5
|
attrs: [],
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
render: ({ content, attrs }, r) => h('div', { ...attrs.attrs }, r(content)),
|
|
7
|
+
editor: (attrs, { slots }) => h('div', { ...attrs }, slots.default()),
|
|
8
|
+
async: true
|
|
9
|
+
},
|
|
10
|
+
div: {
|
|
11
|
+
attrs: [],
|
|
11
12
|
render: ({ content, attrs }, r) => h('div', { ...attrs.attrs }, r(content)),
|
|
12
13
|
editor: (attrs, { slots }) => h('div', { ...attrs }, slots.default()),
|
|
14
|
+
editorClass: (attrs) => [],
|
|
15
|
+
editorContentClass: (attrs) => [attrs.class],
|
|
13
16
|
async: true
|
|
14
17
|
}
|
|
15
18
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { h } from 'vue'
|
|
2
2
|
import { Image } from "@live-change/image-frontend"
|
|
3
|
+
import { parser as javascriptParser } from "@lezer/javascript"
|
|
4
|
+
import { highlightTree } from "@lezer/highlight"
|
|
5
|
+
import { defaultHighlightStyle } from "@codemirror/language"
|
|
3
6
|
|
|
4
7
|
export const basicMarks = {
|
|
5
8
|
bold: (m, c) => h('strong', {}, [ c ]),
|
|
@@ -38,13 +41,26 @@ export const richEditorNodes = {
|
|
|
38
41
|
|
|
39
42
|
heading: ({ content, attrs }, r) => h('h'+(+attrs.level), { }, r(content)),
|
|
40
43
|
blockquote: ({ content }, r) => h('blockquote', { }, r(content)),
|
|
41
|
-
codeBlock: ({ content }, r) =>
|
|
44
|
+
codeBlock: ({ content }, r) => {
|
|
45
|
+
const code = content.map(t => t.text).join('')
|
|
46
|
+
const tree = javascriptParser.parse(code)
|
|
47
|
+
let pos = 0
|
|
48
|
+
let output = []
|
|
49
|
+
highlightTree(tree, defaultHighlightStyle, (from, to, classes) => {
|
|
50
|
+
if(from > pos) output.push(h('span', { }, code.slice(pos, from)))
|
|
51
|
+
//console.log("HIGHLIGHT", from, to, classes, code.slice(from, to))
|
|
52
|
+
output.push(h('span', { class: classes }, code.slice(from, to)))
|
|
53
|
+
pos = to
|
|
54
|
+
})
|
|
55
|
+
if(code.length > pos) output.push(h('span', { }, code.slice(pos)))
|
|
56
|
+
return h('pre', { }, h('code', { }, output))
|
|
57
|
+
},
|
|
42
58
|
|
|
43
59
|
bulletList: ({ content }, r) => h('ul', { }, r(content)),
|
|
44
60
|
orderedList: ({ content }, r) => h('ol', { }, r(content)),
|
|
45
61
|
listItem: ({ content }, r) => h('li', { }, r(content)),
|
|
46
62
|
|
|
47
|
-
image: ({ attrs }) => h(Image, { domResize: 'width', width: 100, ...attrs }),
|
|
63
|
+
image: ({ attrs }) => h(Image, { domResize: 'width', width: 100, class: 'w-full', ...attrs }),
|
|
48
64
|
|
|
49
65
|
...messageNodes
|
|
50
66
|
}
|
|
@@ -56,5 +72,9 @@ export const pageNodes = {
|
|
|
56
72
|
|
|
57
73
|
component: (params, r) => {
|
|
58
74
|
return components[params.attrs.is].render(params, r)
|
|
75
|
+
},
|
|
76
|
+
slot: ({ content, attrs }, r) => {
|
|
77
|
+
/// component slots are not rendered, they are just placeholders, other slots rendered as divs
|
|
78
|
+
return h(attrs.tag ?? 'div', { ...attrs.attrs }, r(content))
|
|
59
79
|
}
|
|
60
80
|
}
|
|
@@ -5,7 +5,7 @@ import HardBreak from "@tiptap/extension-hard-break"
|
|
|
5
5
|
import HorizontalRule from "@tiptap/extension-horizontal-rule"
|
|
6
6
|
import Heading from '@tiptap/extension-heading'
|
|
7
7
|
import Blockqoute from "@tiptap/extension-blockquote"
|
|
8
|
-
import CodeBlock from "@tiptap/extension-code-block"
|
|
8
|
+
//import CodeBlock from "@tiptap/extension-code-block"
|
|
9
9
|
import BulletList from "@tiptap/extension-bullet-list"
|
|
10
10
|
import OrderedList from "@tiptap/extension-ordered-list"
|
|
11
11
|
import ListItem from "@tiptap/extension-list-item"
|
|
@@ -19,7 +19,10 @@ import Strike from "@tiptap/extension-strike"
|
|
|
19
19
|
|
|
20
20
|
import ImageNode from "./ImageNode.js"
|
|
21
21
|
|
|
22
|
+
import CodeBlockNode from "./CodeBlockNode.js";
|
|
23
|
+
|
|
22
24
|
import ComponentNode from "./ComponentNode.js"
|
|
25
|
+
import SlotNode from "./SlotNode.js"
|
|
23
26
|
|
|
24
27
|
export const marksExtensions = {
|
|
25
28
|
bold: [Bold],
|
|
@@ -38,7 +41,8 @@ export const nodesExtensions = {
|
|
|
38
41
|
|
|
39
42
|
heading: [Heading],
|
|
40
43
|
blockquote: [Blockqoute],
|
|
41
|
-
codeBlock: [CodeBlock]
|
|
44
|
+
//codeBlock: [CodeBlock]
|
|
45
|
+
codeBlock: [CodeBlockNode],
|
|
42
46
|
|
|
43
47
|
bulletList: [BulletList],
|
|
44
48
|
orderedList: [OrderedList],
|
|
@@ -46,7 +50,9 @@ export const nodesExtensions = {
|
|
|
46
50
|
|
|
47
51
|
image: [ImageNode],
|
|
48
52
|
|
|
49
|
-
component: [ComponentNode]
|
|
53
|
+
component: [ComponentNode],
|
|
54
|
+
|
|
55
|
+
slot: [SlotNode],
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
export function getExtensions(contentConfig) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
export function previewContent(component, title) {
|
|
3
|
+
return [
|
|
4
|
+
{type: 'heading', attrs:{ level: 3 }, content: [{type: 'text', text: title || component.name }]},
|
|
5
|
+
{type: 'paragraph', content: [{type: 'text', text: component.description }]}
|
|
6
|
+
]
|
|
7
|
+
}
|
|
8
|
+
export function initialContent(component, text) {
|
|
9
|
+
return [
|
|
10
|
+
{type: 'paragraph', content: [{type: 'text', text: text || (component.name + ' content') }]},
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const templates = [
|
|
15
|
+
{
|
|
16
|
+
name: 'Card',
|
|
17
|
+
description: 'PrimeVUE card',
|
|
18
|
+
content({ preview } = {}) {
|
|
19
|
+
return [{
|
|
20
|
+
type: 'component',
|
|
21
|
+
attrs: {
|
|
22
|
+
is: 'div',
|
|
23
|
+
attrs: {
|
|
24
|
+
class: 'surface-card py-1 px-3 shadow-2'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
content: preview ? previewContent(this) : initialContent(this)
|
|
28
|
+
}]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Split',
|
|
33
|
+
description: 'Two columns',
|
|
34
|
+
content({ preview } = {}) {
|
|
35
|
+
return [{
|
|
36
|
+
type: 'component',
|
|
37
|
+
attrs: {
|
|
38
|
+
is: 'div',
|
|
39
|
+
attrs: {
|
|
40
|
+
class: 'flex flex-row flex-wrap'
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'slot',
|
|
46
|
+
attrs: {
|
|
47
|
+
name: 'left',
|
|
48
|
+
attrs: {
|
|
49
|
+
class: preview ? 'w-6' : 'w-12 md:w-6'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
content: preview ? previewContent(this, 'Left') : initialContent(this, 'left')
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'slot',
|
|
56
|
+
attrs: {
|
|
57
|
+
name: 'right',
|
|
58
|
+
attrs: {
|
|
59
|
+
class: preview ? 'w-6' : 'w-12 md:w-6'
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
content: preview ? previewContent(this, 'Right') : initialContent(this, 'right')
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
export default templates
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/wysiwyg-frontend",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"memDev": "lcli memDev --enableSessions --initScript ./init.js --dbAccess",
|
|
6
6
|
"localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"debug": "node --inspect-brk server"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@codemirror/commands": "^6.1.2",
|
|
24
|
+
"@codemirror/lang-javascript": "^6.1.2",
|
|
25
|
+
"@codemirror/language": "^6.1.2",
|
|
26
|
+
"@codemirror/view": "^6.7.1",
|
|
23
27
|
"@fortawesome/fontawesome-free": "^6.2.0",
|
|
24
28
|
"@live-change/cli": "0.7.6",
|
|
25
29
|
"@live-change/dao": "0.5.8",
|
|
@@ -66,6 +70,7 @@
|
|
|
66
70
|
"prismjs": "^1.28.0",
|
|
67
71
|
"prosemirror-collab": "^1.3.0",
|
|
68
72
|
"prosemirror-commands": "^1.3.1",
|
|
73
|
+
"prosemirror-gapcursor": "^1.3.1",
|
|
69
74
|
"prosemirror-history": "^1.3.0",
|
|
70
75
|
"prosemirror-keymap": "^1.2.0",
|
|
71
76
|
"prosemirror-model": "^1.18.1",
|
|
@@ -95,5 +100,5 @@
|
|
|
95
100
|
"author": "",
|
|
96
101
|
"license": "ISC",
|
|
97
102
|
"description": "",
|
|
98
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "c26408cfdc9dfb20a45127dbee1751258edaf4da"
|
|
99
104
|
}
|
|
@@ -32,11 +32,13 @@ module.exports = {
|
|
|
32
32
|
"content": "text*",
|
|
33
33
|
"marks": "",
|
|
34
34
|
"group": "block",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"inline": false,
|
|
36
|
+
"atom": false,
|
|
37
|
+
"selectable": true,
|
|
38
|
+
"draggable": true,
|
|
37
39
|
"attrs": {
|
|
38
40
|
"language": {
|
|
39
|
-
"default":
|
|
41
|
+
"default": "text"
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
},
|
|
@@ -83,7 +85,7 @@ module.exports = {
|
|
|
83
85
|
"selectable": false
|
|
84
86
|
},
|
|
85
87
|
"component": {
|
|
86
|
-
"content": "block*",
|
|
88
|
+
"content": "(block | slot)*",
|
|
87
89
|
"marks": "",
|
|
88
90
|
"group": "block",
|
|
89
91
|
"inline": false,
|
|
@@ -97,6 +99,25 @@ module.exports = {
|
|
|
97
99
|
"default": {}
|
|
98
100
|
}
|
|
99
101
|
}
|
|
102
|
+
},
|
|
103
|
+
"slot": {
|
|
104
|
+
"content": "block*",
|
|
105
|
+
"marks": "",
|
|
106
|
+
"group": "slot",
|
|
107
|
+
"inline": false,
|
|
108
|
+
"selectable": false,
|
|
109
|
+
"draggable": false,
|
|
110
|
+
"attrs": {
|
|
111
|
+
"name": {
|
|
112
|
+
"default": "default"
|
|
113
|
+
},
|
|
114
|
+
"tag": {
|
|
115
|
+
"default": "div"
|
|
116
|
+
},
|
|
117
|
+
"attrs": {
|
|
118
|
+
"default": {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
100
121
|
}
|
|
101
122
|
},
|
|
102
123
|
"topNode": "doc"
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<div class="px-2 pb-2">
|
|
4
|
-
<span class="p-input-icon-left w-full flex">
|
|
5
|
-
<i class="pi pi-search flex-shrink-0" />
|
|
6
|
-
<InputText type="text" v-model="searchQuery" placeholder="Search" class="flex-grow-1" v-focus />
|
|
7
|
-
</span>
|
|
8
|
-
</div>
|
|
9
|
-
<div class="overflow-auto" style="max-height: 75vh">
|
|
10
|
-
<!-- <div v-for="i in [1,2,3,4,5,6,7,8,9,10]">-->
|
|
11
|
-
<div v-for="([ name, component ], index) in searchResults" :key="name"
|
|
12
|
-
class="flex flex-wrap relative cursor-pointer hover:surface-100"
|
|
13
|
-
@click="() => emit('selected', name, component)" >
|
|
14
|
-
<div class="w-3 ml-2 mr-2 my-1" style="aspect-ratio: 1/1">
|
|
15
|
-
<div class="pointer-events-none w-6 absolute p-1"
|
|
16
|
-
style="aspect-ratio: 1/1; transform: scale(0.5); transform-origin: 0 0;">
|
|
17
|
-
<ContentView :config="config" :content="getTestContent(name, component)" style="aspect-ratio: 1/1" />
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="ml-2">
|
|
21
|
-
<h3>{{ name[0].toUpperCase() + name.slice(1) }}</h3>
|
|
22
|
-
<p>{{ component.description }}</p>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
<!-- </div>-->
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
29
|
-
|
|
30
|
-
<script setup>
|
|
31
|
-
import InputText from "primevue/inputtext"
|
|
32
|
-
|
|
33
|
-
import ContentView from "./ContentView.js"
|
|
34
|
-
import components from "./components.js"
|
|
35
|
-
|
|
36
|
-
import { ref, computed } from 'vue'
|
|
37
|
-
|
|
38
|
-
const props = defineProps({
|
|
39
|
-
config: {
|
|
40
|
-
type: Object,
|
|
41
|
-
required: true
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const { config } = props
|
|
46
|
-
|
|
47
|
-
const emit = defineEmits(['selected'])
|
|
48
|
-
|
|
49
|
-
const searchQuery = ref('')
|
|
50
|
-
|
|
51
|
-
const searchResults = computed(() => {
|
|
52
|
-
const query = searchQuery.value.toLowerCase()
|
|
53
|
-
if(!query) return Object.entries(components)
|
|
54
|
-
const byName = Object.entries(components).filter(([name, component]) => {
|
|
55
|
-
return name.toLowerCase().includes(query)
|
|
56
|
-
})
|
|
57
|
-
const byDescription = Object.entries(components).filter(([name, component]) => {
|
|
58
|
-
return (component.description || '').toLowerCase().includes(query)
|
|
59
|
-
}).filter(([name, component]) => !byName.find(([name2]) => name == name2))
|
|
60
|
-
return [...byName, ...byDescription]
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
function getTestContent(name, component) {
|
|
64
|
-
return {
|
|
65
|
-
"type": 'component',
|
|
66
|
-
"attrs": {
|
|
67
|
-
"is": name,
|
|
68
|
-
"attrs": {
|
|
69
|
-
...component.initialAttrs,
|
|
70
|
-
...component.previewAttrs
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
"content": component.previewContent ? [
|
|
74
|
-
{
|
|
75
|
-
"type": "heading",
|
|
76
|
-
"attrs": {
|
|
77
|
-
"level": 3
|
|
78
|
-
},
|
|
79
|
-
"content": [
|
|
80
|
-
{
|
|
81
|
-
"type": "text",
|
|
82
|
-
"text": name[0].toUpperCase() + name.slice(1)
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"type": "paragraph",
|
|
88
|
-
"content": [
|
|
89
|
-
{
|
|
90
|
-
"type": "text",
|
|
91
|
-
"text": component.description
|
|
92
|
-
}
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
] : [
|
|
96
|
-
{
|
|
97
|
-
"type": "text",
|
|
98
|
-
"text": component.description
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
</script>
|
|
105
|
-
|
|
106
|
-
<style scoped>
|
|
107
|
-
|
|
108
|
-
</style>
|