@live-change/wysiwyg-frontend 0.2.9 → 0.2.11

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.
@@ -1,6 +1,6 @@
1
1
  <template>
2
- <div>
3
- <DocumentEditor targetType="Example" :target="target" :type="'rich'" :purpose="'test'"
2
+ <div class="wysiwyg">
3
+ <DocumentEditor targetType="Example" :target="target" :type="'page'" :purpose="'test'"
4
4
  :initialContent="emptyContent"
5
5
  :config="contentConfig" />
6
6
  </div>
@@ -10,7 +10,7 @@
10
10
 
11
11
  import DocumentEditor from "./components/DocumentEditor.vue"
12
12
  import { ref } from 'vue'
13
- import { basicMarks, messageNodes, richEditorNodes } from "./components/contentConfig.js"
13
+ import { basicMarks, messageNodes, richEditorNodes, pageNodes } from "./components/contentConfig.js"
14
14
  import { toRefs } from '@vueuse/core'
15
15
 
16
16
  const props = defineProps({
@@ -28,15 +28,14 @@
28
28
  },
29
29
  nodes: {
30
30
  //...messageNodes,
31
- ...richEditorNodes
31
+ //...richEditorNodes
32
+ ...pageNodes
32
33
  }
33
34
  }
34
35
 
35
- const document = 'testDocument'
36
-
37
36
  const emptyContent = {
38
37
  "type": "doc",
39
- "content": [].concat(new Array(2).fill(
38
+ "content": [
40
39
  {
41
40
  "type": "paragraph",
42
41
  "content": [
@@ -54,12 +53,76 @@
54
53
  "text": "est"
55
54
  }
56
55
  ]
56
+ },
57
+ {
58
+ "type": "component",
59
+ "attrs": {
60
+ "is": "card",
61
+ "attrs": {
62
+ "class": "surface-card px-3 shadow-2 w-full f p-2"
63
+ }
64
+ },
65
+ "content": [
66
+ {
67
+ "type": "paragraph",
68
+ "content": [
69
+ {
70
+ "type": "text",
71
+ "text": "test"
72
+ }
73
+ ]
74
+ }
75
+ ]
76
+ },
77
+ {
78
+ "type": "paragraph",
79
+ "content": [
80
+ {
81
+ "type": "text",
82
+ "text": "test"
83
+ },
84
+ {
85
+ "type": "text",
86
+ "marks": [
87
+ {
88
+ "type": "bold"
89
+ }
90
+ ],
91
+ "text": "est"
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "type": "component",
97
+ "attrs": {
98
+ "is": "card",
99
+ "attrs": {
100
+ "class": "surface-card py-1 px-3 shadow-2 w-full"
101
+ }
102
+ },
103
+ "content": [
104
+ {
105
+ "type": "paragraph",
106
+ "content": [
107
+ {
108
+ "type": "text",
109
+ "text": "test"
110
+ }
111
+ ]
112
+ }
113
+ ]
57
114
  }
58
- ))
115
+ ]
59
116
  }
60
117
 
61
118
  </script>
62
119
 
63
- <style scoped>
120
+ <style lang="scss">
121
+ .wysiwyg {
122
+
123
+ .ProseMirror:focus {
124
+ outline: none;
125
+ }
64
126
 
127
+ }
65
128
  </style>
@@ -1,11 +1,41 @@
1
1
  <template>
2
-
2
+ <ContentView :content="document.content" :config="contentConfig" class="w-full" />
3
+ <pre>{{ JSON.stringify(document.content, null, " ") }}</pre>
3
4
  </template>
4
5
 
5
- <script>
6
- export default {
7
- name: "DocumentPreviewTest"
8
- }
6
+ <script setup>
7
+ import ContentView from "./components/ContentView.js"
8
+ import { ref } from 'vue'
9
+
10
+ const props = defineProps({
11
+ target: {
12
+ type: String,
13
+ default: 'one'
14
+ }
15
+ })
16
+
17
+ import { basicMarks, messageNodes, richEditorNodes, pageNodes } from "./components/contentConfig.js"
18
+
19
+ const contentConfig = {
20
+ marks: {
21
+ ...basicMarks
22
+ },
23
+ nodes: {
24
+ //...messageNodes,
25
+ //...richEditorNodes,
26
+ ...pageNodes
27
+ }
28
+ }
29
+
30
+ import { path, live, useApi } from '@live-change/vue3-ssr'
31
+ const api = useApi()
32
+ const p = path()
33
+
34
+ const document = await live(
35
+ p.prosemirror.document({ targetType: 'Example', target: props.target })
36
+ )
37
+
38
+
9
39
  </script>
10
40
 
11
41
  <style scoped>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <node-view-wrapper>
3
+ <ComponentEditor v-bind="{ ...attrs, class: ['relative', attrs.class] }">
4
+ <node-view-content class="content" />
5
+ <div class="absolute top-0 right-0 pr-4 max-h-0 align-items-center z-5 edit-buttons ">
6
+ <Button icon="pi pi-eye"
7
+ :class="[
8
+ 'p-button p-button-icon-only p-button-rounded mr-0',
9
+ selectedEditor != 'style' ? 'p-button-success' : 'p-button-secondary'
10
+ ]"
11
+ style="transform: scale(0.8)"
12
+ @click="(event) => toggleEditor('style', event)" />
13
+ <Button icon="pi pi-cog"
14
+ :class="[
15
+ 'p-button p-button-icon-only p-button-rounded mr-0',
16
+ selectedEditor != 'settings' ? 'p-button-warning' : 'p-button-secondary'
17
+ ]"
18
+ style="transform: scale(0.8)"
19
+ @click="(event) => toggleEditor('settings', event)"/>
20
+ <Button icon="pi pi-trash"
21
+ class="p-button p-button-icon-only p-button-rounded p-button-danger"
22
+ style="transform: scale(0.8)"
23
+ @click="() => props.deleteNode()" />
24
+ </div>
25
+ <div class="absolute pointer-events-none border-2 border-dashed z-9"
26
+ :class="selectedEditor == 'style' ? 'border-blue-400' : 'border-yellow-400'"
27
+ style="top: -10px; left: -10px; width: auto; height: auto; right: -10px; bottom: -10px"
28
+ v-if="selectedEditor"></div>
29
+ </ComponentEditor>
30
+ </node-view-wrapper>
31
+ </template>
32
+
33
+ <script setup>
34
+ import Button from "primevue/button"
35
+
36
+ import { NodeViewContent, nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
37
+ import components from "./components.js";
38
+
39
+ import { inject, computed, getCurrentInstance } from "vue"
40
+
41
+ const props = defineProps(nodeViewProps)
42
+
43
+ const is = computed(() => props.node.attrs.is)
44
+ const attrs = computed(() => props.node.attrs.attrs)
45
+
46
+ const ComponentEditor = components[props.node.attrs.is].editor
47
+
48
+ const uid = getCurrentInstance().uid
49
+
50
+ const nodeControl = computed(() => ({
51
+ uid,
52
+ updateAttributes: props.updateAttributes,
53
+ deleteNode: props.deleteNode,
54
+ is: props.node.attrs.is,
55
+ attrs: props.node.attrs.attrs
56
+ }))
57
+
58
+ const editorControl = inject("componentEditorControl")
59
+
60
+ function toggleEditor(type, event) {
61
+ editorControl.toggleEditor(type, nodeControl, event)
62
+ }
63
+
64
+ const selectedEditor = computed(() =>{
65
+ console.log('EC', editorControl.componentControl.value?.nodeControl?.value.uid, uid)
66
+ return editorControl.componentControl.value?.nodeControl?.value?.uid === uid
67
+ ? editorControl.componentControl.value?.type
68
+ : null
69
+ })
70
+
71
+ </script>
72
+
73
+ <style lang="scss">
74
+ .edit-buttons {
75
+ display: none;
76
+ }
77
+ .show-edit-buttons .edit-buttons {
78
+ display: flex;
79
+ }
80
+
81
+ </style>
@@ -0,0 +1,74 @@
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: 'component',
7
+ group: 'block',
8
+ //atom: true,
9
+ inline: false,
10
+ selectable: true,
11
+ draggable: true,
12
+ content: 'block*',
13
+ marks: '',
14
+
15
+ addAttributes() {
16
+ return {
17
+ is: {
18
+ default: 'card',
19
+ renderHTML: attributes => {
20
+ return {
21
+ 'data-is': attributes.is
22
+ }
23
+ },
24
+ },
25
+ attrs: {
26
+ default: {},
27
+ renderHTML: attributes => {
28
+ const htmlAttrs = {}
29
+ const attrs = attributes.attrs
30
+ for(let key in this.options) {
31
+ htmlAttrs[`data-attr-${key}`] = attrs[key]
32
+ }
33
+ return htmlAttrs
34
+ },
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({ HTMLAttributes }) {
50
+ /*const htmlAttrs = {}
51
+ const attrs = this.options.attrs
52
+ for(let key in this.options) {
53
+ htmlAttrs[`data-attr-${key}`] = attrs[key]
54
+ }
55
+ htmlAttrs['data-is'] = this.options.is
56
+ return ['component', mergeAttributes(HTMLAttributes, htmlAttrs)]*/
57
+ return ['component', HTMLAttributes]
58
+ },
59
+
60
+ addNodeView() {
61
+ return VueNodeViewRenderer(ComponentComponent)
62
+ },
63
+
64
+ addCommands() {
65
+ return {
66
+ setImage: options => ({ commands }) => {
67
+ return commands.insertContent({
68
+ type: this.name,
69
+ attrs: options
70
+ })
71
+ }
72
+ }
73
+ }
74
+ })
@@ -0,0 +1,108 @@
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>
@@ -9,9 +9,9 @@ function renderNode(node, r, marks, nodes) {
9
9
  }
10
10
 
11
11
  function ContentView(props, context) {
12
- const { content, config } = props
12
+ let { content, config } = props
13
13
  if(content) {
14
- const doc = JSON.parse(content)
14
+ const doc = typeof content == 'string' ? JSON.parse(content) : content
15
15
  const r = nodes => nodes?.map(n => renderNode(n, r, config.marks, config.nodes))
16
16
  return renderNode(doc, r, config.marks, config.nodes)
17
17
  }
@@ -3,20 +3,42 @@
3
3
  <slot v-if="editor" name="menu" :editor="editor" :saveState="saveState">
4
4
  <EditorMenu :editor="editor" :config="config" :saveState="saveState">
5
5
  <template #before="scope"><slot name="beforeMenu" v-bind="{ ...scope, editor, saveState }" /></template>
6
- <template #after="scope"><slot name="afterMenu" v-bind="{ ...scope, editor, saveState }" /></template>
7
6
  <template #begin="scope"><slot name="menuBegin" v-bind="{ ...scope, editor, saveState }" /></template>
8
- <template #end="scope"><slot name="menuEnd" v-bind="{ ...scope, editor, saveState }" /></template>
7
+ <template #end="scope">
8
+ <slot name="menuEnd" v-bind="{ ...scope, editor, saveState }" />
9
+ <Button v-if="config.nodes.component" label="Edit Buttons"
10
+ :class="[
11
+ 'p-button-sm p-button-primary inline-block mr-1 mb-1', { 'p-button-outlined': !editButtons }
12
+ ]"
13
+ @click="toggleEditButtons" />
14
+ </template>
15
+ <template #after="scope">
16
+ <div v-if="componentControl"
17
+ class="surface-card p-1 shadow-2 border-round">
18
+ <StyleEditor v-if="componentControl.type == 'style'" :nodeControl="componentControl.nodeControl"
19
+ :key="componentControl.uid" />
20
+ <SettingsEditor v-if="componentControl.type == 'settings'" :nodeControl="componentControl.nodeControl"
21
+ :key="componentControl.uid" />
22
+ </div>
23
+ <slot name="afterMenu" v-bind="{ ...scope, editor, saveState }" />
24
+ </template>
9
25
  </EditorMenu>
10
26
  </slot>
11
- <editor-content :editor="editor" class="content" />
27
+ <editor-content :editor="editor" :class="[content, { 'show-edit-buttons': editButtons }]" />
12
28
  </div>
13
29
  </template>
14
30
 
15
31
  <script setup>
32
+ import Button from "primevue/button"
33
+
34
+ import StyleEditor from "./StyleEditor.vue";
35
+ import SettingsEditor from "./SettingsEditor.vue";
16
36
 
17
37
  import { useEditor, EditorContent } from '@tiptap/vue-3'
38
+ import { History } from '@tiptap/extension-history'
18
39
  import {
19
- ref, computed, watch, provide, defineEmits, defineProps, getCurrentInstance, onUnmounted, inject, onMounted
40
+ ref, computed, watch, provide, defineEmits, defineProps, getCurrentInstance, onUnmounted, inject, onMounted,
41
+ shallowRef
20
42
  } from 'vue'
21
43
  import { toRefs, useDebounceFn } from '@vueuse/core'
22
44
  import EditorMenu from "./EditorMenu.vue"
@@ -119,6 +141,10 @@
119
141
  ProseMirrorCollab.configure({
120
142
  version: documentData.version,
121
143
  clientID
144
+ }),
145
+ History.configure({
146
+ depth: 100,
147
+ newGroupDelay: 500
122
148
  })
123
149
  ],
124
150
  onTransaction: ({ editor, transaction }) => {
@@ -134,6 +160,16 @@
134
160
  ? serializeSchema(editor.view.state.schema.spec)
135
161
  : getSchemaSpecFromConfig(props.config)
136
162
 
163
+ const editButtons = ref(true)
164
+ function toggleEditButtons() {
165
+ editButtons.value = !editButtons.value
166
+ if(!editButtons.value) {
167
+ componentControl.value = null
168
+ }
169
+ }
170
+
171
+ if(typeof window != 'undefined') window.schemaSpecJson = JSON.stringify(schemaSpec, null, " ")
172
+
137
173
  async function save() {
138
174
  const state = editor.value.reactiveState.value
139
175
  const sendable = sendableSteps(state)
@@ -155,6 +191,21 @@
155
191
  transaction.apply(editor.value.view.state)
156
192
  }
157
193
  }
194
+
195
+ const componentControl = shallowRef()
196
+ provide('componentEditorControl', {
197
+ componentControl,
198
+ toggleEditor(type, nodeControl, event) {
199
+ const uid = nodeControl.value.uid
200
+ if(componentControl.value && componentControl.value.uid == uid && componentControl.value.type == type) {
201
+ componentControl.value = null
202
+ } else {
203
+ console.log("SET COMPONENT CONTROL", nodeControl.value)
204
+ componentControl.value = { type, nodeControl, uid }
205
+ }
206
+ }
207
+ })
208
+
158
209
  </script>
159
210
 
160
211
  <style scoped>
@@ -15,6 +15,7 @@
15
15
  <script setup>
16
16
 
17
17
  import { useEditor, EditorContent } from '@tiptap/vue-3'
18
+ import { History } from '@tiptap/extension-history'
18
19
 
19
20
  import { ref, computed, watch, provide, defineEmits, defineProps } from 'vue'
20
21
  import { toRefs } from '@vueuse/core'
@@ -39,7 +40,13 @@
39
40
 
40
41
  const editor = useEditor({
41
42
  content: JSON.parse(props.modelValue),
42
- extensions,
43
+ extensions: [
44
+ ...extensions,
45
+ History.configure({
46
+ depth: 100,
47
+ newGroupDelay: 500
48
+ })
49
+ ],
43
50
  onUpdate: ({ editor, transaction }) => {
44
51
  console.log("EDITOR UPDATE", editor, transaction)
45
52
  const editorJson = JSON.stringify(editor.getJSON())
@@ -76,8 +76,9 @@
76
76
  </FileInput>
77
77
  <!-- </div>-->
78
78
 
79
- <Button v-if="config.nodes.component" label="Insert Component" class="p-button-sm p-button-primary"
80
- @click="editor.chain().focus().setHorizontalRule().run()" />
79
+ <Button v-if="config.nodes.component" label="Component"
80
+ class="p-button-sm p-button-primary p-button-outlined inline-block mr-1 mb-1"
81
+ @click="openComponentMenu" />
81
82
 
82
83
  <span v-if="saveState"
83
84
  class="p-button p-component p-button-sm p-button-outlined p-button-secondary cursor-auto
@@ -98,14 +99,23 @@
98
99
 
99
100
  </div>
100
101
  <slot name="after" />
102
+ <OverlayPanel ref="insertComponentOverlay" style="width: 450px" :breakpoints="{'960px': '75vw'}">
103
+ <ComponentsMenu :config="config" @selected="(n,c) => handleComponentSelected(n,c)" />
104
+ </OverlayPanel>
105
+ <!-- <div class="surface-card p-1 shadow-2" style="width: 450px">
106
+ <h3>components:</h3>
107
+ <ComponentsMenu :config="config" />
108
+ </div>-->
101
109
  </div>
102
110
  </template>
103
111
 
104
112
  <script setup>
105
113
  import Button from "primevue/button"
106
114
  import ProgressSpinner from "primevue/progressspinner"
115
+ import OverlayPanel from 'primevue/overlaypanel'
107
116
 
108
- import { inject, getCurrentInstance } from "vue"
117
+ import ComponentsMenu from "./ComponentsMenu.vue"
118
+ import { inject, getCurrentInstance, ref } from "vue"
109
119
  import { toRefs } from "@vueuse/core"
110
120
  import { uploadImage } from "@live-change/image-frontend"
111
121
  import { FileInput } from "@live-change/upload-frontend"
@@ -145,7 +155,28 @@
145
155
  await upload.upload()
146
156
  }
147
157
 
148
- window.editor = editor.value
158
+ const insertComponentOverlay = ref()
159
+ function openComponentMenu(event) {
160
+ insertComponentOverlay.value.show(event)
161
+ }
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()
176
+ //editor.value.chain().focus().setNode('component', { is: component }).run()
177
+ }
178
+
179
+ if(typeof window != 'undefined') window.editor = editor.value
149
180
  </script>
150
181
 
151
182
  <style scoped>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <h3>Empty component</h3>
3
+ </template>
4
+
5
+ <script setup>
6
+
7
+ </script>
8
+
9
+ <style scoped>
10
+
11
+ </style>
@@ -2,7 +2,6 @@ import { Node } from '@tiptap/core'
2
2
  import { VueNodeViewRenderer } from '@tiptap/vue-3'
3
3
  import ImageComponent from "./ImageComponent.vue"
4
4
 
5
-
6
5
  export default Node.create({
7
6
  name: 'image',
8
7
  group: 'block',
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div class="flex flex-row flex-wrap ">
3
+ <div v-for="type in paddingTypes"
4
+ class="flex flex-row align-items-center justify-content-start my-1"
5
+ style="width: 5.6em">
6
+ <label :for="'padding-'+type" class="mb-0 mr-1">{{ type }}</label>
7
+ <InputNumber v-if="props.modelValue[type] !== undefined"
8
+ :inputId="'padding-'+type"
9
+ class="p-inputtext-sm"
10
+ inputStyle="max-width: 1.7em;"
11
+ :modelValue="props.modelValue[type]"
12
+ @update:modelValue="value => updatePadding(type, value)"
13
+ mode="decimal"
14
+ showButtons :min="-1" :max="8" />
15
+ <Button v-else class="p-button p-button-icon-only p-button-rounded mr-0" icon="pi pi-plus"
16
+ @click="ev => updatePadding(type, 1)" />
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup>
22
+ import InputNumber from "primevue/inputnumber"
23
+ import Button from "primevue/button"
24
+
25
+ const props = defineProps({
26
+ modelValue: {
27
+ type: Object,
28
+ default: () => ({})
29
+ }
30
+ })
31
+ const emit = defineEmits(['update:modelValue'])
32
+
33
+ const paddingTypes = ['all', 'x', 'y', 'l', 'r', 't', 'b']
34
+
35
+ function updatePadding(type, value) {
36
+ console.log("UPDATE PADDING", type, value)
37
+ const newValue = { ...props.modelValue }
38
+ if(value < 0) {
39
+ delete newValue[type]
40
+ } else {
41
+ newValue[type] = value
42
+ }
43
+ emit('update:modelValue', newValue)
44
+ }
45
+ </script>
46
+
47
+ <style scoped>
48
+
49
+ </style>
@@ -117,6 +117,7 @@ class RemoteAuthority{
117
117
  debug("LOAD DOCUMENT!")
118
118
  const identifier = { targetType: this.targetType, target: this.target }
119
119
  let documentData = await this.api.get(['prosemirror', 'document', identifier])
120
+ console.log("DOCUMENT DATA", documentData)
120
121
  if(!documentData) {
121
122
  documentData = {
122
123
  ...identifier,
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <h2>Settings editor</h2>
3
+ </template>
4
+
5
+ <script setup>
6
+ </script>
7
+
8
+ <style scoped>
9
+
10
+ </style>
@@ -0,0 +1,179 @@
1
+ <template>
2
+ <!-- <pre>{{ JSON.stringify(classInfo, null, " ") }}</pre>-->
3
+ <div class="flex flex-column flex-wrap w-full">
4
+ <div v-if="editingPadding">
5
+ <div class="flex flex-row align-items-center mb-2">
6
+ <label class="mr-3" style="width: 4em">Padding:</label>
7
+ <PaddingEditor :modelValue="classInfo.padding"
8
+ @update:modelValue="p => handleClassInfoUpdate({ padding: p })" />
9
+ </div>
10
+ <div class="flex flex-row align-items-center">
11
+ <label class="mr-3" style="width: 4em">Margin:</label>
12
+ <PaddingEditor :modelValue="classInfo.margin"
13
+ @update:modelValue="p => handleClassInfoUpdate({ margin: p })" />
14
+ </div>
15
+ </div>
16
+ <div class="flex flex-column md:flex-row">
17
+ <div class="flex-1 mr-2 flex flex-column">
18
+ <div class="flex flex-row align-items-center flex-shrink-0">
19
+ <label class="my-2 flex-grow-1">Classes:</label>
20
+ <div class="flex flex-row align-items-center mr-4">
21
+ <i class="pi pi-arrows-alt" style="font-size: 1.2rem"></i>
22
+ <InputSwitch v-model="editingPadding" class="ml-2" />
23
+ </div>
24
+ </div>
25
+ <AutoComplete :modelValue="classInfo.list"
26
+ @update:modelValue="l => handleClassInfoUpdate({ list: l })"
27
+ :separator="/ /" :multiple="true" :suggestions="classSuggestions"
28
+ @complete="ev => searchClasses(ev)"
29
+ class="autocomplete-w-full flex-grow-1" />
30
+ </div>
31
+ <div class="flex-1 flex flex-column">
32
+ <div class="flex flex-row flex-shrink-0">
33
+ <label class="my-2 flex-grow-1">Style:</label>
34
+ </div>
35
+ <prism-editor v-if="isMounted"
36
+ class="style-editor p-inputtext flex-grow-1" :highlight="highlightCss"
37
+ :style="{ height: (nodeStyleLines * 1.35 + 1.1) + 'em' }"
38
+ :modelValue="nodeStyle" @update:modelValue="handleStyleUpdate"
39
+ :readonly="false" :line-numbers="nodeStyleLines > 1" />
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup>
46
+ import AutoComplete from "primevue/autocomplete"
47
+ import InputSwitch from 'primevue/inputswitch';
48
+
49
+ import PaddingEditor from "./PaddingEditor.vue"
50
+
51
+ import 'vue-prism-editor/dist/prismeditor.min.css'
52
+ import 'prismjs/themes/prism-coy.css'
53
+ import * as Prism from 'prismjs/components/prism-core'
54
+ import 'prismjs/components/prism-css'
55
+
56
+ import { PrismEditor } from 'vue-prism-editor'
57
+
58
+ import {ref, computed, watch, provide, defineEmits, defineProps, onMounted} from 'vue'
59
+ import { toRefs } from '@vueuse/core'
60
+
61
+ const isMounted = ref(false)
62
+ onMounted(() => isMounted.value = true)
63
+
64
+ const props = defineProps({
65
+ nodeControl: {
66
+ type: Object,
67
+ required: true
68
+ }
69
+ })
70
+
71
+ const { nodeControl } = toRefs(props)
72
+
73
+ const editingPadding = ref(false)
74
+
75
+ function extract(info, regex, setter) {
76
+ const rest = []
77
+ const list = info.list
78
+ for(let i = 0; i < list.length; i++) {
79
+ const match = list[i].match(regex)
80
+ if(match) setter(...match.slice(1))
81
+ else rest.push(list[i])
82
+ }
83
+ info.list = rest
84
+ }
85
+
86
+ function extractClasses(info) {
87
+ info.padding = { }
88
+ if(editingPadding.value) {
89
+ extract(info, /^p([xyltrb])?-(\d+)$/, (dir, size) => {
90
+ if (dir) info.padding[dir] = size
91
+ else info.padding.all = size
92
+ })
93
+ info.margin = {}
94
+ extract(info, /^m([xyltrb])?-(\d+)$/, (dir, size) => {
95
+ if (dir) info.margin[dir] = size
96
+ else info.margin.all = size
97
+ })
98
+ }
99
+ }
100
+ function recombinePadding(list, padding, prefix = 'p') {
101
+ if(padding.all !== undefined) list.push(`${prefix}-${padding.all}`)
102
+ if(padding.x !== undefined) list.push(`${prefix}x-${padding.x}`)
103
+ if(padding.y !== undefined) list.push(`${prefix}y-${padding.y}`)
104
+ if(padding.t !== undefined) list.push(`${prefix}t-${padding.t}`)
105
+ if(padding.r !== undefined) list.push(`${prefix}r-${padding.r}`)
106
+ if(padding.b !== undefined) list.push(`${prefix}b-${padding.b}`)
107
+ if(padding.l !== undefined) list.push(`${prefix}l-${padding.l}`)
108
+ }
109
+ function recombineClasses(info) {
110
+ const { padding, margin, list } = info
111
+ if(editingPadding.value) {
112
+ recombinePadding(list, padding, 'p')
113
+ recombinePadding(list, margin, 'm')
114
+ }
115
+ }
116
+
117
+ const insertableClasses = ['flex', 'flex-grow-1']
118
+ const nodeClass = computed(() => {
119
+ console.log("nodeControl", nodeControl.value)
120
+ return nodeControl.value.attrs.class
121
+ })
122
+ const classInfo = computed(() => {
123
+ const list = nodeClass.value ? nodeClass.value.split(' ') : []
124
+ const info = { list }
125
+ extractClasses(info)
126
+ return info
127
+ })
128
+ const classSuggestions = ref()
129
+ function searchClasses(event) {
130
+ const query = event.query.toLowerCase()
131
+ console.log("SEARCH CLASSES", query)
132
+ classSuggestions.value = [
133
+ ...(insertableClasses.find(c => c == event.query) ? [] : [event.query]),
134
+ ...(insertableClasses.filter((c) => {
135
+ return c.toLowerCase().startsWith(query)
136
+ }))
137
+ ]
138
+ }
139
+ function handleClassInfoUpdate(change) {
140
+ let info = JSON.parse(JSON.stringify(classInfo.value))
141
+ info = { ...info, ...change }
142
+ recombineClasses(info)
143
+ console.log("INFO", info)
144
+ nodeControl.value.updateAttributes({ attrs: {
145
+ ...nodeControl.value.attrs,
146
+ class: info.list.join(' ')
147
+ }})
148
+ }
149
+
150
+ const nodeStyle = computed(() => {
151
+ return nodeControl.value.attrs.style
152
+ })
153
+ const nodeStyleLines = computed(() => (nodeStyle.value || '').split('\n').length)
154
+ function highlightCss(code) {
155
+ return Prism.highlight(code, Prism.languages.css, "css")
156
+ }
157
+ function handleStyleUpdate(style) {
158
+ console.log("NC", nodeControl.value)
159
+ nodeControl.value.updateAttributes({ attrs: {
160
+ ...nodeControl.value.attrs,
161
+ style
162
+ }})
163
+ }
164
+
165
+ </script>
166
+
167
+ <style lang="scss">
168
+ .autocomplete-w-full > ul {
169
+ width: 100%;
170
+ }
171
+ .prism-editor__textarea {
172
+ outline: 0 none;
173
+ outline-offset: 0;
174
+ }
175
+ .style-editor:focus-within {
176
+ box-shadow: 0 0 0 0.2rem #b7e0b8;
177
+ border-color: #4CAF50;
178
+ }
179
+ </style>
@@ -0,0 +1,17 @@
1
+ import { h } from 'vue'
2
+
3
+ const components = {
4
+ card: {
5
+ description: 'PrimeVUE card',
6
+ previewContent: true,
7
+ attrs: [],
8
+ initialAttrs: {
9
+ class: 'surface-card py-1 px-3 shadow-2'
10
+ },
11
+ render: ({ content, attrs }, r) => h('div', { ...attrs.attrs }, r(content)),
12
+ editor: (attrs, { slots }) => h('div', { ...attrs }, slots.default()),
13
+ async: true
14
+ }
15
+ }
16
+
17
+ export default components
@@ -48,3 +48,13 @@ export const richEditorNodes = {
48
48
 
49
49
  ...messageNodes
50
50
  }
51
+
52
+ import components from './components.js'
53
+
54
+ export const pageNodes = {
55
+ ...richEditorNodes,
56
+
57
+ component: (params, r) => {
58
+ return components[params.attrs.is].render(params, r)
59
+ }
60
+ }
@@ -19,6 +19,7 @@ import Strike from "@tiptap/extension-strike"
19
19
 
20
20
  import ImageNode from "./ImageNode.js"
21
21
 
22
+ import ComponentNode from "./ComponentNode.js"
22
23
 
23
24
  export const marksExtensions = {
24
25
  bold: [Bold],
@@ -43,7 +44,9 @@ export const nodesExtensions = {
43
44
  orderedList: [OrderedList],
44
45
  listItem: [ListItem],
45
46
 
46
- image: [ImageNode]
47
+ image: [ImageNode],
48
+
49
+ component: [ComponentNode]
47
50
  }
48
51
 
49
52
  export function getExtensions(contentConfig) {
@@ -18,10 +18,15 @@ export function wysiwygRoutes(config = {}) {
18
18
  }),
19
19
 
20
20
  route({
21
- name: 'wysiwyg:documentEditor', path: prefix + 'collab', meta: { },
21
+ name: 'wysiwyg:documentEditor', path: prefix + 'doc/:target', meta: { },
22
22
  component: () => import("./DocumentEditorTest.vue"),
23
- props: {
24
- }
23
+ props: true
24
+ }),
25
+
26
+ route({
27
+ name: 'wysiwyg:documentPreview', path: prefix + 'view/:target', meta: { },
28
+ component: () => import("./DocumentPreviewTest.vue"),
29
+ props: true
25
30
  }),
26
31
 
27
32
  ...dbAdminRoutes({ prefix: '/_db', route: r => ({ ...r, meta: { ...r.meta, raw: true }}) })
package/index.js CHANGED
@@ -11,3 +11,7 @@ import EditorMenu from "./front/src/components/EditorMenu.vue"
11
11
  import ImageComponent from "./front/src/components/ImageComponent.vue"
12
12
  import ImageNode from "./front/src/components/ImageNode.js"
13
13
  export { DocumentEditor, Editor, EditorMenu, ImageComponent, ImageNode }
14
+
15
+ import components from "./front/src/components/components.js"
16
+
17
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/wysiwyg-frontend",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",
@@ -21,16 +21,16 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@fortawesome/fontawesome-free": "^6.2.0",
24
- "@live-change/cli": "0.7.4",
24
+ "@live-change/cli": "0.7.5",
25
25
  "@live-change/dao": "0.5.8",
26
26
  "@live-change/dao-vue3": "0.5.8",
27
27
  "@live-change/dao-websocket": "0.5.8",
28
- "@live-change/framework": "0.7.4",
29
- "@live-change/password-authentication-service": "0.3.2",
30
- "@live-change/prosemirror-service": "0.3.2",
31
- "@live-change/secret-code-service": "0.3.2",
32
- "@live-change/secret-link-service": "0.3.2",
33
- "@live-change/session-service": "0.3.2",
28
+ "@live-change/framework": "0.7.5",
29
+ "@live-change/password-authentication-service": "0.3.4",
30
+ "@live-change/prosemirror-service": "0.3.4",
31
+ "@live-change/secret-code-service": "0.3.4",
32
+ "@live-change/secret-link-service": "0.3.4",
33
+ "@live-change/session-service": "0.3.4",
34
34
  "@live-change/vue3-components": "0.2.16",
35
35
  "@live-change/vue3-ssr": "0.2.16",
36
36
  "@tiptap/extension-blockquote": "^2.0.0-beta.29",
@@ -62,18 +62,19 @@
62
62
  "primeflex": "^3.2.1",
63
63
  "primeicons": "^6.0.1",
64
64
  "primevue": "^3.18.1",
65
+ "prismjs": "^1.28.0",
65
66
  "prosemirror-collab": "^1.3.0",
66
67
  "rollup-plugin-node-builtins": "^2.1.2",
67
68
  "rollup-plugin-visualizer": "5.6.0",
68
- "serialize-javascript": "^6.0.0",
69
69
  "serve-static": "^1.15.0",
70
70
  "v-shared-element": "3.1.0",
71
71
  "vue-meta": "^3.0.0-alpha.9",
72
+ "vue-prism-editor": "2.0.0-alpha.2",
72
73
  "vue-router": "^4.1.3",
73
74
  "vue3-scroll-border": "0.1.2"
74
75
  },
75
76
  "devDependencies": {
76
- "@live-change/codeceptjs-helper": "0.7.4",
77
+ "@live-change/codeceptjs-helper": "0.7.5",
77
78
  "@wdio/selenium-standalone-service": "^7.20.8",
78
79
  "codeceptjs": "^3.3.4",
79
80
  "generate-password": "1.7.0",
@@ -85,5 +86,5 @@
85
86
  "author": "",
86
87
  "license": "ISC",
87
88
  "description": "",
88
- "gitHead": "703c9723562ade7b2420e1b25b28ced51a96db09"
89
+ "gitHead": "7b0e7fc149a3d419aa125b45314cf4eac2fc9410"
89
90
  }
@@ -0,0 +1,103 @@
1
+ module.exports = {
2
+ "marks": {
3
+ "bold": {},
4
+ "italic": {},
5
+ "underline": {},
6
+ "strike": {}
7
+ },
8
+ "nodes": {
9
+ "paragraph": {
10
+ "content": "inline*",
11
+ "group": "block"
12
+ },
13
+ "horizontalRule": {
14
+ "group": "block"
15
+ },
16
+ "heading": {
17
+ "content": "inline*",
18
+ "group": "block",
19
+ "defining": true,
20
+ "attrs": {
21
+ "level": {
22
+ "default": 1
23
+ }
24
+ }
25
+ },
26
+ "blockquote": {
27
+ "content": "block+",
28
+ "group": "block",
29
+ "defining": true
30
+ },
31
+ "codeBlock": {
32
+ "content": "text*",
33
+ "marks": "",
34
+ "group": "block",
35
+ "code": true,
36
+ "defining": true,
37
+ "attrs": {
38
+ "language": {
39
+ "default": null
40
+ }
41
+ }
42
+ },
43
+ "bulletList": {
44
+ "content": "listItem+",
45
+ "group": "block list"
46
+ },
47
+ "orderedList": {
48
+ "content": "listItem+",
49
+ "group": "block list",
50
+ "attrs": {
51
+ "start": {
52
+ "default": 1
53
+ }
54
+ }
55
+ },
56
+ "listItem": {
57
+ "content": "paragraph block*",
58
+ "defining": true
59
+ },
60
+ "image": {
61
+ "content": "",
62
+ "marks": "",
63
+ "group": "block",
64
+ "inline": false,
65
+ "atom": true,
66
+ "selectable": true,
67
+ "draggable": true,
68
+ "attrs": {
69
+ "image": {
70
+ "default": null
71
+ }
72
+ }
73
+ },
74
+ "doc": {
75
+ "content": "block+"
76
+ },
77
+ "text": {
78
+ "group": "inline"
79
+ },
80
+ "hardBreak": {
81
+ "group": "inline",
82
+ "inline": true,
83
+ "selectable": false
84
+ },
85
+ "component": {
86
+ "content": "block*",
87
+ "marks": "",
88
+ "group": "block",
89
+ "inline": false,
90
+ "selectable": true,
91
+ "draggable": true,
92
+ "attrs": {
93
+ "is": {
94
+ "default": "card"
95
+ },
96
+ "attrs": {
97
+ "default": {}
98
+ }
99
+ }
100
+ }
101
+ },
102
+ "topNode": "doc"
103
+ }
@@ -17,7 +17,8 @@ module.exports = {
17
17
  name: 'prosemirror',
18
18
  path: '@live-change/prosemirror-service',
19
19
  documentTypes: {
20
- rich: require('./rich.documentType.js')
20
+ rich: require('./rich.documentType.js'),
21
+ page: require('./page.documentType.js')
21
22
  },
22
23
  testLatency: 1000
23
24
  }