@live-change/wysiwyg-frontend 0.2.9 → 0.2.10

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,12 +28,11 @@
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
38
  "content": [].concat(new Array(2).fill(
@@ -60,6 +59,12 @@
60
59
 
61
60
  </script>
62
61
 
63
- <style scoped>
62
+ <style lang="scss">
63
+ .wysiwyg {
64
+
65
+ .ProseMirror:focus {
66
+ outline: none;
67
+ }
64
68
 
69
+ }
65
70
  </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,22 @@
1
+ <template>
2
+ <node-view-wrapper>
3
+ <ComponentEditor>
4
+ <node-view-content class="content" />
5
+ </ComponentEditor>
6
+ </node-view-wrapper>
7
+ </template>
8
+
9
+ <script setup>
10
+ import { NodeViewContent, nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
11
+ import components from "./components.js";
12
+
13
+ const props = defineProps(nodeViewProps)
14
+
15
+ const { is, attrs } = props.node.attrs
16
+
17
+ const ComponentEditor = components[is].editor
18
+ </script>
19
+
20
+ <style scoped>
21
+
22
+ </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,107 @@
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)" >
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.previewAttrs
70
+ }
71
+ },
72
+ "content": component.previewContent ? [
73
+ {
74
+ "type": "heading",
75
+ "attrs": {
76
+ "level": 3
77
+ },
78
+ "content": [
79
+ {
80
+ "type": "text",
81
+ "text": name[0].toUpperCase() + name.slice(1)
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ "type": "paragraph",
87
+ "content": [
88
+ {
89
+ "type": "text",
90
+ "text": component.description
91
+ }
92
+ ]
93
+ }
94
+ ] : [
95
+ {
96
+ "type": "text",
97
+ "text": component.description
98
+ }
99
+ ]
100
+ }
101
+ }
102
+
103
+ </script>
104
+
105
+ <style scoped>
106
+
107
+ </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
  }
@@ -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
  import {
19
20
  ref, computed, watch, provide, defineEmits, defineProps, getCurrentInstance, onUnmounted, inject, onMounted
20
21
  } from 'vue'
@@ -119,6 +120,10 @@
119
120
  ProseMirrorCollab.configure({
120
121
  version: documentData.version,
121
122
  clientID
123
+ }),
124
+ History.configure({
125
+ depth: 100,
126
+ newGroupDelay: 500
122
127
  })
123
128
  ],
124
129
  onTransaction: ({ editor, transaction }) => {
@@ -134,6 +139,8 @@
134
139
  ? serializeSchema(editor.view.state.schema.spec)
135
140
  : getSchemaSpecFromConfig(props.config)
136
141
 
142
+ if(typeof window != 'undefined') window.schemaSpecJson = JSON.stringify(schemaSpec, null, " ")
143
+
137
144
  async function save() {
138
145
  const state = editor.value.reactiveState.value
139
146
  const sendable = sendableSteps(state)
@@ -155,6 +162,7 @@
155
162
  transaction.apply(editor.value.view.state)
156
163
  }
157
164
  }
165
+
158
166
  </script>
159
167
 
160
168
  <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="Insert Component"
80
+ class="p-button-sm p-button-primary 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="c => handleComponentSelected(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,27 @@
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(component) {
163
+ insertComponentOverlay.value.hide()
164
+ editor.value.chain().focus().insertContent({
165
+ type: 'component',
166
+ attrs: {
167
+ is: component,
168
+ },
169
+ content: [
170
+ { type: 'paragraph', content: [
171
+ { type: 'text', text: 'test' }
172
+ ] }
173
+ ]
174
+ }).run()
175
+ //editor.value.chain().focus().setNode('component', { is: component }).run()
176
+ }
177
+
178
+ if(typeof window != 'undefined') window.editor = editor.value
149
179
  </script>
150
180
 
151
181
  <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',
@@ -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,15 @@
1
+ import { h } from 'vue'
2
+
3
+ const components = {
4
+ card: {
5
+ description: 'PrimeVUE card',
6
+ previewContent: true,
7
+ attrs: [],
8
+ render: ({ content, attrs }, r) => h('div', { 'class': 'surface-card py-1 px-3 shadow-2 w-full' }, r(content)),
9
+ editor: (props, { slots }) => h('div', { 'class': 'surface-card py-1 px-3 shadow-2 w-full' }, slots.default()),
10
+ async: true
11
+ }
12
+ }
13
+
14
+
15
+ 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.10",
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.3",
30
+ "@live-change/prosemirror-service": "0.3.3",
31
+ "@live-change/secret-code-service": "0.3.3",
32
+ "@live-change/secret-link-service": "0.3.3",
33
+ "@live-change/session-service": "0.3.3",
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",
@@ -73,7 +73,7 @@
73
73
  "vue3-scroll-border": "0.1.2"
74
74
  },
75
75
  "devDependencies": {
76
- "@live-change/codeceptjs-helper": "0.7.4",
76
+ "@live-change/codeceptjs-helper": "0.7.5",
77
77
  "@wdio/selenium-standalone-service": "^7.20.8",
78
78
  "codeceptjs": "^3.3.4",
79
79
  "generate-password": "1.7.0",
@@ -85,5 +85,5 @@
85
85
  "author": "",
86
86
  "license": "ISC",
87
87
  "description": "",
88
- "gitHead": "703c9723562ade7b2420e1b25b28ced51a96db09"
88
+ "gitHead": "7d4c56cc610d692df963c3916411ceb46663b344"
89
89
  }
@@ -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
  }