From 42fc42c3ed7d2d3a09b09882ff876416b731a8b6 Mon Sep 17 00:00:00 2001 From: myml Date: Mon, 4 Jul 2022 13:19:19 +0800 Subject: [PATCH 1/3] feat: markdown support paste image --- client/components/editor/editor-markdown.vue | 98 ++++++++++++++++---- 1 file changed, 82 insertions(+), 16 deletions(-) diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index 04b5c6aa..376b217e 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -170,8 +170,12 @@ import _ from 'lodash' import { get, sync } from 'vuex-pathify' import markdownHelp from './markdown/help.vue' import gql from 'graphql-tag' +import Cookies from 'js-cookie' import DOMPurify from 'dompurify' +import listFolderAssetQuery from 'gql/editor/editor-media-query-folder-list.gql' +import createAssetFolderMutation from 'gql/editor/editor-media-mutation-folder-create.gql' + /* global siteConfig, siteLangs */ // ======================================== @@ -432,22 +436,84 @@ export default { onCmInput: _.debounce(function (newContent) { this.processContent(newContent) }, 600), - onCmPaste (cm, ev) { - // const clipItems = (ev.clipboardData || ev.originalEvent.clipboardData).items - // for (let clipItem of clipItems) { - // if (_.startsWith(clipItem.type, 'image/')) { - // const file = clipItem.getAsFile() - // const reader = new FileReader() - // reader.onload = evt => { - // this.$store.commit(`loadingStart`, 'editor-paste-image') - // this.insertAfter({ - // content: `![${file.name}](${evt.target.result})`, - // newLine: true - // }) - // } - // reader.readAsDataURL(file) - // } - // } + async onCmPaste (cm, ev) { + const clipItems = (ev.clipboardData || ev.originalEvent.clipboardData).items + for (let clipItem of clipItems) { + if (_.startsWith(clipItem.type, 'image/')) { + // get parent directory + const folderPath = '/' + this.path.split('/').slice(0,-1).join('/') + let folderID = 0 + try { + folderID = await this.createFolder(folderPath) + } catch (err) { + this.$store.commit('pushGraphError', err) + return + } + const file = clipItem.getAsFile() + const now = new Date() + // file name format yyyy-MM-dd_random.png + let filename = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}_${parseInt(Math.random() * (10**5))}` + filename += file.name.slice(file.name.lastIndexOf('.')) + const form = new FormData() + form.append('mediaUpload', JSON.stringify({ 'folderId': folderID })) + form.append('mediaUpload', file, filename) + const jwtToken = Cookies.get('jwt') + this.$store.commit('showNotification', { + message: this.$t('editor:assets.uploading'), + style: 'primary', + icon: 'primary' + }) + try { + const resp = await fetch('/u', { method: 'POST', body: form, headers: { 'Authorization': `Bearer ${jwtToken}` }}) + if( resp.status != 200 ){ + throw resp.statusText + } + this.insertAtCursor({content: `![${filename}](${folderPath}/${filename})`}) + } catch (err) { + return this.$store.commit('showNotification', { + message: this.$t('editor:assets.uploadFailed'), + style: 'error', + icon: 'error' + }) + } + } + } + }, + async createFolder(path) { + let folderID = 0 + for(const folderName of path.split("/").slice(1)){ + let folders = await this.getSubFolder(folderID) + let folder = folders.find(folder => folder.slug === folderName) + if (!folder) { + const resp = await this.$apollo.mutate({ + mutation: createAssetFolderMutation, + variables: { + parentFolderId: folderID, + slug: folderName + } + }) + if (! _.get(resp, 'data.assets.createFolder.responseResult.succeeded', false)) { + const message = _.get(resp, 'data.assets.createFolder.responseResult.message') + this.$store.commit('pushGraphError', new Error(message)) + throw new Error(message); + } + folders = await this.getSubFolder(folderID) + console.log("getSubFolder", folders) + folder = folders.find(folder => folder.slug === folderName) + } + folderID = folder.id + } + return folderID + }, + async getSubFolder(folderID){ + const resp = await this.$apollo.query({ + query: listFolderAssetQuery, + variables: { + parentFolderId: folderID, + }, + fetchPolicy: 'network-only' + }) + return _.get(resp, "data.assets.folders", []) }, processContent (newContent) { linesMap = [] From 2808ad7a6d73bdad6b59d0581e3a38d7b943c497 Mon Sep 17 00:00:00 2001 From: wurongjie Date: Mon, 3 Apr 2023 13:52:07 +0800 Subject: [PATCH 2/3] fix: api case-insensitive folder name --- client/components/editor/editor-markdown.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index 376b217e..70e9b8e3 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -483,7 +483,8 @@ export default { let folderID = 0 for(const folderName of path.split("/").slice(1)){ let folders = await this.getSubFolder(folderID) - let folder = folders.find(folder => folder.slug === folderName) + // api case-insensitive for folder name + let folder = folders.find(folder => folder.slug.toLowerCase() === folderName.toLowerCase()) if (!folder) { const resp = await this.$apollo.mutate({ mutation: createAssetFolderMutation, @@ -499,7 +500,7 @@ export default { } folders = await this.getSubFolder(folderID) console.log("getSubFolder", folders) - folder = folders.find(folder => folder.slug === folderName) + folder = folders.find(folder => folder.slug.toLowerCase() === folderName.toLowerCase()) } folderID = folder.id } From 05bea5774d00ac8756a01034bcf1a7b3507343b4 Mon Sep 17 00:00:00 2001 From: wurongjie Date: Fri, 7 Apr 2023 15:25:57 +0800 Subject: [PATCH 3/3] fix: multi-language environment and browser compatible After 'Multilingual Namespacing' is enable, lang_code should be add to the image path, but if the image path has lang_code prefix, it will return 404 So the pasted image are storage in /assets/$lang/$md_path In Chromium kernel the paste event will be released after callback return Since the callback async function, the files must be get before 'await' keyword --- client/components/editor/editor-markdown.vue | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index 70e9b8e3..247cdb5d 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -438,22 +438,30 @@ export default { }, 600), async onCmPaste (cm, ev) { const clipItems = (ev.clipboardData || ev.originalEvent.clipboardData).items - for (let clipItem of clipItems) { - if (_.startsWith(clipItem.type, 'image/')) { + // In Chromium kernel the paste event will be released after callback return + // Since this is an async function, the file must be get before 'await' keyword + const files = Array.from(clipItems).filter(item=>_.startsWith(item.type, 'image/')).map(item=>item.getAsFile()) + // upload all file + for (const file of files) { // get parent directory - const folderPath = '/' + this.path.split('/').slice(0,-1).join('/') let folderID = 0 + let folderPath = "/assets/" + this.locale try { + if(this.path.includes("/")){ + // path to lowercase, the wikijs foldre does not support case sensitivity. + folderPath += "/" + this.path.toLowerCase().split('/').slice(0,-1).join('/') + } + // try to get the folder id, create it if does not exists. folderID = await this.createFolder(folderPath) } catch (err) { this.$store.commit('pushGraphError', err) return } - const file = clipItem.getAsFile() const now = new Date() // file name format yyyy-MM-dd_random.png let filename = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}_${parseInt(Math.random() * (10**5))}` filename += file.name.slice(file.name.lastIndexOf('.')) + // upload file const form = new FormData() form.append('mediaUpload', JSON.stringify({ 'folderId': folderID })) form.append('mediaUpload', file, filename) @@ -476,7 +484,6 @@ export default { icon: 'error' }) } - } } }, async createFolder(path) { @@ -484,7 +491,7 @@ export default { for(const folderName of path.split("/").slice(1)){ let folders = await this.getSubFolder(folderID) // api case-insensitive for folder name - let folder = folders.find(folder => folder.slug.toLowerCase() === folderName.toLowerCase()) + let folder = folders.find(folder => folder.slug === folderName) if (!folder) { const resp = await this.$apollo.mutate({ mutation: createAssetFolderMutation, @@ -499,8 +506,7 @@ export default { throw new Error(message); } folders = await this.getSubFolder(folderID) - console.log("getSubFolder", folders) - folder = folders.find(folder => folder.slug.toLowerCase() === folderName.toLowerCase()) + folder = folders.find(folder => folder.slug === folderName) } folderID = folder.id }