@live-change/wysiwyg-frontend 0.2.19 → 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 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="{ ...attrs, class: ['relative', attrs.class] }">
4
- <node-view-content class="content" />
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 ComponentEditor = components[props.node.attrs.is].editor
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
- setImage: options => ({ commands }) => {
67
- return commands.insertContent({
68
- type: this.name,
69
- attrs: options
70
- })
71
- }
66
+
72
67
  }
73
68
  }
74
69
  })
@@ -18,6 +18,7 @@ function ContentView(props, context) {
18
18
  return h('div', {}, [
19
19
  h('h1', {}, 'Empty document'),
20
20
  ])
21
+
21
22
  }
22
23
 
23
24
  export default ContentView
@@ -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.error("RECEIVE STEPS", steps, clientIDs, clientID)
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.error("SEND STEPS", sendable.steps, sendable.version)
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.error("SEND STEPS", sendable.steps, sendable.version)
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 scoped>
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: 1">
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('codeblock') }"
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="Component"
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="openComponentMenu" />
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="insertComponentOverlay" style="width: 450px" :breakpoints="{'960px': '75vw'}">
103
- <ComponentsMenu :config="config" @selected="(n,c) => handleComponentSelected(n,c)" />
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
- <ComponentsMenu :config="config" />
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 ComponentsMenu from "./ComponentsMenu.vue"
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 insertComponentOverlay = ref()
159
- function openComponentMenu(event) {
160
- insertComponentOverlay.value.show(event)
163
+ const insertTemplateOverlay = ref()
164
+ function openTemplateMenu(event) {
165
+ insertTemplateOverlay.value.show(event)
161
166
  }
162
- function handleComponentSelected(name, component) {
163
- insertComponentOverlay.value.hide()
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>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <node-view-wrapper>
3
- <Image :image="image" width="100" dom-resize="width" />
3
+ <Image :image="image" width="100" dom-resize="width" class="w-full" />
4
4
  </node-view-wrapper>
5
5
  </template>
6
6
 
@@ -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
- initialAttrs: {
9
- class: 'surface-card py-1 px-3 shadow-2'
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) => h('pre', { }, h('code', { }, r(content))),
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.19",
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": "55b604864ff3fdcc7ab8b4d7665fea92cda0e235"
103
+ "gitHead": "c26408cfdc9dfb20a45127dbee1751258edaf4da"
99
104
  }
@@ -32,11 +32,13 @@ module.exports = {
32
32
  "content": "text*",
33
33
  "marks": "",
34
34
  "group": "block",
35
- "code": true,
36
- "defining": true,
35
+ "inline": false,
36
+ "atom": false,
37
+ "selectable": true,
38
+ "draggable": true,
37
39
  "attrs": {
38
40
  "language": {
39
- "default": null
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>