Sfoglia il codice sorgente

feat: table editor (wip) + markdown editor improvements

Nicolas Giard 2 anni fa
parent
commit
9fb9f53d53

+ 6 - 0
ux/package-lock.json

@@ -64,6 +64,7 @@
         "quasar": "2.11.5",
         "quasar": "2.11.5",
         "slugify": "1.6.5",
         "slugify": "1.6.5",
         "socket.io-client": "4.5.4",
         "socket.io-client": "4.5.4",
+        "tabulator-tables": "5.4.4",
         "tippy.js": "6.3.7",
         "tippy.js": "6.3.7",
         "uuid": "9.0.0",
         "uuid": "9.0.0",
         "v-network-graph": "0.8.1",
         "v-network-graph": "0.8.1",
@@ -6714,6 +6715,11 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/tabulator-tables": {
+      "version": "5.4.4",
+      "resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-5.4.4.tgz",
+      "integrity": "sha512-WqPWLRwrD8UMISUjQqZyj6y9k3ajQBs7eJtRosbt9q8RRkZlYCJYGxLt3L44jEXaQCE5q5EJFbGeDUEDJa1a3w=="
+    },
     "node_modules/tar-stream": {
     "node_modules/tar-stream": {
       "version": "2.2.0",
       "version": "2.2.0",
       "dev": true,
       "dev": true,

+ 1 - 0
ux/package.json

@@ -67,6 +67,7 @@
     "quasar": "2.11.5",
     "quasar": "2.11.5",
     "slugify": "1.6.5",
     "slugify": "1.6.5",
     "socket.io-client": "4.5.4",
     "socket.io-client": "4.5.4",
+    "tabulator-tables": "5.4.4",
     "tippy.js": "6.3.7",
     "tippy.js": "6.3.7",
     "uuid": "9.0.0",
     "uuid": "9.0.0",
     "v-network-graph": "0.8.1",
     "v-network-graph": "0.8.1",

+ 1 - 0
ux/public/_assets/icons/color-data-grid.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#64B5F6" d="M42,37c0,2.762-2.238,5-5,5H11c-2.761,0-5-2.238-5-5V11c0-2.762,2.239-5,5-5h26c2.762,0,5,2.238,5,5V37z"/><path fill="#E3F2FD" d="M17 18h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C18 17.552 17.552 18 17 18M37 18h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C38 17.552 37.552 18 37 18M27 18h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C28 17.552 27.552 18 27 18"/><path fill="#E3F2FD" d="M17 18h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C18 17.552 17.552 18 17 18M37 28h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C38 27.552 37.552 28 37 28M27 28h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C28 27.552 27.552 28 27 28M17 28h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C18 27.552 17.552 28 17 28M37 38h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C38 37.552 37.552 38 37 38M27 38h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C28 37.552 27.552 38 27 38M17 38h-6c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h6c.552 0 1 .448 1 1v6C18 37.552 17.552 38 17 38"/></svg>

+ 63 - 2
ux/src/components/EditorMarkdown.vue

@@ -15,6 +15,7 @@
         icon='mdi-image-plus-outline'
         icon='mdi-image-plus-outline'
         padding='sm sm'
         padding='sm sm'
         flat
         flat
+        @click='insertAssets'
         )
         )
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertAssets') }}
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertAssets') }}
       q-btn(
       q-btn(
@@ -27,6 +28,7 @@
         icon='mdi-table-large-plus'
         icon='mdi-table-large-plus'
         padding='sm sm'
         padding='sm sm'
         flat
         flat
+        @click='insertTable'
         )
         )
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertTable') }}
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertTable') }}
       q-btn(
       q-btn(
@@ -39,8 +41,11 @@
         icon='mdi-line-scan'
         icon='mdi-line-scan'
         padding='sm sm'
         padding='sm sm'
         flat
         flat
+        @click='insertHorizontalBar'
         )
         )
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.horizontalBar') }}
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.horizontalBar') }}
+      q-space
+      span.editor-markdown-type Markdown
     .editor-markdown-mid
     .editor-markdown-mid
       //--------------------------------------------------------
       //--------------------------------------------------------
       //- TOP TOOLBAR
       //- TOP TOOLBAR
@@ -112,6 +117,14 @@
           flat
           flat
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.keyboardKey') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.keyboardKey') }}
+        q-btn(
+          v-if='!state.previewShown'
+          icon='mdi-eye-arrow-right-outline'
+          padding='xs sm'
+          flat
+          @click='state.previewShown = true'
+          )
+          q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.togglePreviewPane') }}
       //--------------------------------------------------------
       //--------------------------------------------------------
       //- CODEMIRROR
       //- CODEMIRROR
       //--------------------------------------------------------
       //--------------------------------------------------------
@@ -126,12 +139,15 @@
             icon='mdi-arrow-vertical-lock'
             icon='mdi-arrow-vertical-lock'
             padding='xs sm'
             padding='xs sm'
             flat
             flat
+            @click='state.previewScrollSync = !state.previewScrollSync'
+            :color='state.previewScrollSync ? `primary` : null'
             )
             )
-            q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.toggleScrollLock') }}
+            q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.toggleScrollSync') }}
           q-btn(
           q-btn(
             icon='mdi-eye-off-outline'
             icon='mdi-eye-off-outline'
             padding='xs sm'
             padding='xs sm'
             flat
             flat
+            @click='state.previewShown = false'
             )
             )
             q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.togglePreviewPane') }}
             q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.togglePreviewPane') }}
         .editor-markdown-preview-content.contents(ref='editorPreviewContainer')
         .editor-markdown-preview-content.contents(ref='editorPreviewContainer')
@@ -147,6 +163,7 @@ import { useMeta, useQuasar, setCssVar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 
 
 import { useEditorStore } from 'src/stores/editor'
 import { useEditorStore } from 'src/stores/editor'
+import { useSiteStore } from 'src/stores/site'
 
 
 // Code Mirror
 // Code Mirror
 import CodeMirror from 'codemirror'
 import CodeMirror from 'codemirror'
@@ -174,6 +191,7 @@ const $q = useQuasar()
 // STORES
 // STORES
 
 
 const editorStore = useEditorStore()
 const editorStore = useEditorStore()
+const siteStore = useSiteStore()
 
 
 // I18N
 // I18N
 
 
@@ -187,12 +205,48 @@ const cmRef = ref(null)
 const state = reactive({
 const state = reactive({
   content: '',
   content: '',
   previewShown: true,
   previewShown: true,
-  previewHTML: ''
+  previewHTML: '',
+  previewScrollSync: false
 })
 })
 
 
 // Platform detection
 // Platform detection
 const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
 const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
 
 
+// METHODS
+
+function insertAssets () {
+  siteStore.$patch({
+    overlay: 'FileManager'
+  })
+}
+
+function insertTable () {
+  siteStore.$patch({
+    overlay: 'TableEditor'
+  })
+}
+
+/**
+ * Insert content at cursor
+ */
+function insertAtCursor ({ content }) {
+  const cursor = cm.value.doc.getCursor('head')
+  cm.value.doc.replaceRange(content, cursor)
+}
+
+/**
+ * Insert content after current line
+ */
+function insertAfter ({ content, newLine }) {
+  const curLine = cm.value.doc.getCursor('to').line
+  const lineLength = cm.value.doc.getLine(curLine).length
+  cm.value.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
+}
+
+function insertHorizontalBar () {
+  insertAfter({ content: '---', newLine: true })
+}
+
 // MOUNTED
 // MOUNTED
 
 
 onMounted(async () => {
 onMounted(async () => {
@@ -339,6 +393,7 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     display: block;
     display: block;
     height: $editor-height;
     height: $editor-height;
     position: relative;
     position: relative;
+    border-right: 5px solid $primary;
   }
   }
   &-editor {
   &-editor {
     display: block;
     display: block;
@@ -348,6 +403,12 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     //   height: $editor-height-mobile;
     //   height: $editor-height-mobile;
     // }
     // }
   }
   }
+  &-type {
+    writing-mode: vertical-rl;
+    text-orientation: mixed;
+    padding-bottom: 1rem;
+    color: rgba(255,255,255, .4);
+  }
   &-preview {
   &-preview {
     flex: 1 1 50%;
     flex: 1 1 50%;
     background-color: $grey-2;
     background-color: $grey-2;

+ 4 - 0
ux/src/components/MainOverlayDialog.vue

@@ -22,6 +22,10 @@ const overlays = {
   FileManager: defineAsyncComponent({
   FileManager: defineAsyncComponent({
     loader: () => import('./FileManager.vue'),
     loader: () => import('./FileManager.vue'),
     loadingComponent: LoadingGeneric
     loadingComponent: LoadingGeneric
+  }),
+  TableEditor: defineAsyncComponent({
+    loader: () => import('./TableEditorOverlay.vue'),
+    loadingComponent: LoadingGeneric
   })
   })
 }
 }
 
 

+ 2 - 0
ux/src/components/PageHeader.vue

@@ -10,6 +10,8 @@
       color='primary'
       color='primary'
       flat
       flat
       )
       )
+      q-badge(color='grey' floating rounded)
+        q-icon(name='las la-pen', size='xs', padding='xs xs')
       q-menu(content-class='shadow-7')
       q-menu(content-class='shadow-7')
         icon-picker-dialog(v-model='pageStore.icon')
         icon-picker-dialog(v-model='pageStore.icon')
     q-icon.rounded-borders(
     q-icon.rounded-borders(

+ 87 - 0
ux/src/components/TableEditorOverlay.vue

@@ -0,0 +1,87 @@
+<template lang="pug">
+q-layout(view='hHh lpR fFf', container)
+  q-header.card-header.q-px-md.q-py-sm
+    q-icon(name='img:/_assets/icons/color-data-grid.svg', left, size='md')
+    span {{t(`editor.tableEditor.title`)}}
+    q-space
+    q-btn.q-mr-sm(
+      flat
+      rounded
+      color='white'
+      :aria-label='t(`common.actions.refresh`)'
+      icon='las la-question-circle'
+      :href='siteStore.docsBase + `/admin/editors/markdown`'
+      target='_blank'
+      type='a'
+    )
+    q-btn-group(push)
+      q-btn(
+        push
+        color='white'
+        text-color='grey-7'
+        :label='t(`common.actions.cancel`)'
+        :aria-label='t(`common.actions.cancel`)'
+        icon='las la-times'
+        @click='close'
+      )
+      q-btn(
+        push
+        color='positive'
+        text-color='white'
+        :label='t(`common.actions.save`)'
+        :aria-label='t(`common.actions.save`)'
+        icon='las la-check'
+        :disabled='state.loading > 0'
+      )
+  q-page-container
+    q-page.q-pa-md
+      div(ref='tblRef')
+
+      q-inner-loading(:showing='state.loading > 0')
+        q-spinner(color='accent', size='lg')
+</template>
+
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { useQuasar } from 'quasar'
+import { onMounted, reactive, ref } from 'vue'
+import { Tabulator } from 'tabulator-tables'
+import gql from 'graphql-tag'
+import { cloneDeep } from 'lodash-es'
+
+import { useSiteStore } from 'src/stores/site'
+
+import 'tabulator-tables/dist/css/tabulator.css'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  loading: 0
+})
+const tblRef = ref(null)
+
+// METHODS
+
+function close () {
+  siteStore.$patch({ overlay: '' })
+}
+
+onMounted(() => {
+  const tbl = new Tabulator(tblRef.value, {
+    clipboard: true,
+    height: '100%'
+  })
+})
+</script>

+ 3 - 2
ux/src/i18n/locales/en.json

@@ -1717,7 +1717,8 @@
   "profile.cvdTritanopia": "Tritanopia",
   "profile.cvdTritanopia": "Tritanopia",
   "profile.cvdDeuteranopia": "Deuteranopia",
   "profile.cvdDeuteranopia": "Deuteranopia",
   "profile.accessibility": "Accessibility",
   "profile.accessibility": "Accessibility",
-  "editor.toggleScrollLock": "Toggle Scroll Lock",
+  "editor.toggleScrollSync": "Toggle Scroll Sync",
   "editor.togglePreviewPane": "Toggle Preview Pane",
   "editor.togglePreviewPane": "Toggle Preview Pane",
-  "editor.renderPreview": "Render Preview"
+  "editor.renderPreview": "Render Preview",
+  "editor.tableEditor.title": "Table Editor"
 }
 }

+ 3 - 1
ux/src/layouts/MainLayout.vue

@@ -52,7 +52,7 @@ q-layout(view='hHh Lpr lff')
           q-item-section(side)
           q-item-section(side)
             q-icon(name='las la-cat', color='white')
             q-icon(name='las la-cat', color='white')
           q-item-section Installation
           q-item-section Installation
-    q-bar.bg-blue-9.text-white(dense)
+    q-bar.bg-blue-9.text-white(dense, v-if='flagsStore.experimental')
       q-btn.col(
       q-btn.col(
         icon='las la-dharmachakra'
         icon='las la-dharmachakra'
         label='History'
         label='History'
@@ -88,6 +88,7 @@ import { useRouter, useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 
 
 import { useEditorStore } from 'src/stores/editor'
 import { useEditorStore } from 'src/stores/editor'
+import { useFlagsStore } from 'src/stores/flags'
 import { useSiteStore } from 'src/stores/site'
 import { useSiteStore } from 'src/stores/site'
 
 
 // COMPONENTS
 // COMPONENTS
@@ -103,6 +104,7 @@ const $q = useQuasar()
 // STORES
 // STORES
 
 
 const editorStore = useEditorStore()
 const editorStore = useEditorStore()
+const flagsStore = useFlagsStore()
 const siteStore = useSiteStore()
 const siteStore = useSiteStore()
 
 
 // ROUTER
 // ROUTER