Skip to content

Commit

Permalink
Merge pull request #879 from nextcloud/enh/link-to-files
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusknorr authored Aug 12, 2020
2 parents 32ebfc3 + a3dcb4f commit 69690ff
Show file tree
Hide file tree
Showing 32 changed files with 424 additions and 70 deletions.
26 changes: 24 additions & 2 deletions js/editor-rich.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/editor-rich.js.map

Large diffs are not rendered by default.

41 changes: 31 additions & 10 deletions js/editor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/editor.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions js/files.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/files.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/public.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/public.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/text.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/text.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/vendors~editor-collab~editor-guest.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/vendors~editor-collab~editor-guest.js.map

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions js/vendors~editor-rich.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/vendors~editor-rich.js.map

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions js/vendors~editor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/vendors~editor.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/vendors~editor~files-modal.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/vendors~editor~files-modal.js.map

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions js/vendors~files-modal.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/vendors~files-modal.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/viewer.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/viewer.js.map

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/components/EditorWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
<slot name="header" />
</MenuBar>
<div>
<MenuBubble v-if="!readOnly && isRichEditor" :editor="tiptap" />
<MenuBubble v-if="!readOnly && isRichEditor"
:editor="tiptap"
:filePath="relativePath" />
<EditorContent v-show="initialLoading"
class="editor__content"
:editor="tiptap" />
Expand Down
25 changes: 23 additions & 2 deletions src/components/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import { EditorMenuBar } from 'tiptap'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import menuBarIcons from './../mixins/menubar'
import { optimalPath } from './../helpers/files'

import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
Expand Down Expand Up @@ -244,7 +245,7 @@ export default {
return
}
const _command = command
OC.dialogs.filepicker('Insert an image', (file) => {
OC.dialogs.filepicker(t('text', 'Insert an image'), (file) => {
const client = OC.Files.getClient()
client.getFileInfo(file).then((_status, fileInfo) => {
this.lastImagePath = fileInfo.path
Expand All @@ -254,7 +255,7 @@ export default {
mimetype: fileInfo.mimetype,
hasPreview: fileInfo.hasPreview,
}
const path = this.optimalPathTo(`${fileInfo.path}/${fileInfo.name}`)
const path = optimalPath(this.filePath, `${fileInfo.path}/${fileInfo.name}`)
const encodedPath = path.split('/').map(encodeURIComponent).join('/')
const meta = Object.entries(appendMeta).map(([key, val]) => `${key}=${encodeURIComponent(val)}`).join('&')
const src = `${encodedPath}?fileId=${fileInfo.id}#${meta}`
Expand All @@ -266,6 +267,26 @@ export default {
})
}, false, [], true, undefined, this.imagePath)
},
showLinkPrompt(command) {
const currentUser = OC.getCurrentUser()
if (!currentUser) {
return
}
const _command = command
OC.dialogs.filepicker('Insert a link', (file) => {
const client = OC.Files.getClient()
client.getFileInfo(file).then((_status, fileInfo) => {
this.lastLinkPath = fileInfo.path
const path = this.optimalPathTo(`${fileInfo.path}/${fileInfo.name}`)
const encodedPath = path.split('/').map(encodeURIComponent).join('/')
const href = `${encodedPath}?fileId=${fileInfo.id}`

_command({
href,
})
})
}, false, [], true, undefined, this.linkPath)
},
optimalPathTo(targetFile) {
const absolutePath = targetFile.split('/')
const relativePath = this.relativePathTo(targetFile).split('/')
Expand Down
52 changes: 47 additions & 5 deletions src/components/MenuBubble.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,28 @@
type="text"
placeholder="https://"
@keydown.esc="hideLinkMenu">
<button class="menububble__button icon-confirm" type="button" @click="setLinkUrl(commands.link, linkUrl)" />
<button class="menububble__button icon-confirm"
type="button"
tabindex="0"
@click="setLinkUrl(commands.link, linkUrl)" />
</form>

<template v-else>
<button
class="menububble__button"
:class="{ 'is-active': isActive.link() }"
@click="showLinkMenu(getMarkAttrs('link'))">
<span v-tooltip="isActive.link() ? 'Update Link' : 'Add Link'" class="icon-link" />
<span class="menububble__buttontext">{{ t('text', 'Add link') }}</span>
<span v-tooltip="t('text', isActive.link() ? 'Update Link' : 'Add Link')" class="icon-link" />
<span class="menububble__buttontext">
{{ t('text', isActive.link() ? 'Update Link' : 'Add Link') }}
</span>
</button>
<button
class="menububble__button"
:class="{ 'is-active': isActive.link() }"
@click="selectFile(commands.link)">
<span v-tooltip="t('text', 'Link file')" class="icon-file" />
<span class="menububble__buttontext">{{ t('text', 'Link file') }}</span>
</button>
</template>
</div>
Expand All @@ -52,6 +64,7 @@
<script>
import { EditorMenuBubble } from 'tiptap'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import { optimalPath } from './../helpers/files'

export default {
name: 'MenuBubble',
Expand All @@ -67,6 +80,11 @@ export default {
required: false,
default: null,
},
filePath: {
type: String,
required: false,
default: '',
},
},
data: () => {
return {
Expand All @@ -86,9 +104,33 @@ export default {
this.linkUrl = null
this.linkMenuIsActive = false
},

selectFile(command) {
const currentUser = OC.getCurrentUser()
if (!currentUser) {
return
}
const startPath = this.filePath.split('/').slice(0, -1).join('/')
OC.dialogs.filepicker(t('text', 'Select file to link to'), (file) => {
const client = OC.Files.getClient()
client.getFileInfo(file).then((_status, fileInfo) => {
const path = optimalPath(this.filePath, `${fileInfo.path}/${fileInfo.name}`)
const encodedPath = path.split('/').map(encodeURIComponent).join('/')
command({ href: `${encodedPath}?fileId=${fileInfo.id}` })
this.hideLinkMenu()
})
}, false, [], true, undefined, startPath)
},
setLinkUrl(command, url) {
if (url && !url.match(/^[a-zA-Z]+:\/\//) && !url.match(/^\//)) {
// Heuristics for determining if we need a https:// prefix.
const noPrefixes = [
/^[a-zA-Z]+:/, // url with protocol ("mailTo:email@domain.tld")
/^\//, // absolute path
/\?fileId=/, // relative link with fileId
/^\.\.?\//, // relative link starting with ./ or ../
/^[^.]*[/$]/, // no dots before first '/' - not a domain name
/^#/, // url fragment
]
if (url && !noPrefixes.find(regex => url.match(regex))) {
url = 'https://' + url
}
command({ href: url })
Expand Down
20 changes: 16 additions & 4 deletions src/helpers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,27 @@
*
*/

/**
* Callback that should be executed after the document is ready
* @param callback
*/
import { openMimetypes } from './mime'
import RichWorkspace from '../views/RichWorkspace'
import { imagePath } from '@nextcloud/router'

const FILE_ACTION_IDENTIFIER = 'Edit with text app'

const optimalPath = function(from, to) {
const current = from.split('/')
const target = to.split('/')
current.pop() // ignore filename
while (current[0] === target[0]) {
current.shift()
target.shift()
}
const relativePath = current.fill('..').concat(target)
const absolutePath = to.split('/')
return relativePath.length < absolutePath.length
? relativePath.join('/')
: to
}

const registerFileCreate = () => {
const newFileMenuPlugin = {
attach(menu) {
Expand Down Expand Up @@ -157,6 +168,7 @@ const FilesWorkspacePlugin = {
}

export {
optimalPath,
registerFileActionFallback,
registerFileCreate,
FilesWorkspacePlugin,
Expand Down
83 changes: 83 additions & 0 deletions src/helpers/links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* @copyright Copyright (c) 2020 Azul <azul@riseup.net>
*
* @author Azul <azul@riseup.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { generateUrl } from '@nextcloud/router'

const absolutePath = function(base, rel) {
if (!rel) {
return base
}
if (rel[0] === '/') {
return rel
}
base = base.split('/')
rel = rel.split('/')
while (rel[0] === '..' || rel[0] === '.') {
if (rel[0] === '..') {
base.pop()
}
rel.shift()
}
return base.concat(rel).join('/')
}

const basedir = function(file) {
const end = file.lastIndexOf('/')
return (end > 0)
? file.slice(0, end)
: file.slice(0, end + 1) // basedir('/toplevel') should return '/'
}

const domHref = function(node) {
const ref = node.attrs.href
if (!ref) {
return ref
}
if (ref.match(/^[a-zA-Z]*:/)) {
return ref
}
const match = ref.match(/^([^?]*)\?fileId=(\d+)/)
if (match) {
const [, relPath, id] = match
const currentDir = basedir(OCA.Viewer.state.file)
const dir = absolutePath(currentDir, basedir(relPath))
return generateUrl(`/apps/files/?dir=${dir}&openfile=${id}#relPath=${relPath}`)
}
}

const parseHref = function(dom) {
const ref = dom.getAttribute('href')
if (!ref) {
return ref
}
const match = ref.match(/\?dir=([^&]*)&openfile=([^&]*)#relPath=([^&]*)/)
if (match) {
const [, , id, path] = match
return `${path}?fileId=${id}`
}
return ref
}

export {
domHref,
parseHref,
}
47 changes: 46 additions & 1 deletion src/marks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
*/

import { Bold, Italic as TipTapItalic, Strike as TipTapStrike, Link as TipTapLink } from 'tiptap-extensions'
import { Plugin } from 'tiptap'
import { getMarkAttrs } from 'tiptap-utils'
import { domHref, parseHref } from './../helpers/links'

/**
* This file maps prosemirror mark names to tiptap classes,
Expand Down Expand Up @@ -88,18 +91,60 @@ class Link extends TipTapLink {
{
tag: 'a[href]',
getAttrs: dom => ({
href: dom.getAttribute('href'),
href: parseHref(dom),
}),
},
],
toDOM: node => ['a', {
...node.attrs,
href: domHref(node),
title: node.attrs.href,
rel: 'noopener noreferrer nofollow',
}, 0],
}
}

get plugins() {
if (!this.options.openOnClick) {
return []
}

return [
new Plugin({
props: {
handleClick: (view, pos, event) => {
const { schema } = view.state
const attrs = getMarkAttrs(view.state, schema.marks.link)

if (attrs.href && event.target instanceof HTMLAnchorElement) {
event.stopPropagation()
const htmlHref = event.target.href
if (event.button === 0 && !event.ctrlKey && htmlHref.startsWith(window.location.origin)) {
const query = OC.parseQueryString(htmlHref)
const fragment = OC.parseQueryString(htmlHref.split('#').pop())
if (query.dir && fragment.relPath) {
const filename = fragment.relPath.split('/').pop()
const path = `${query.dir}/${filename}`
document.title = `${filename} - ${OC.theme.title}`
if (window.location.pathname.match(/apps\/files\/$/)) {
// The files app still lacks a popState handler
// to allow for using the back button
// OC.Util.History.pushState('', htmlHref)
}
OCA.Viewer.open({ path })
} else {
window.open(htmlHref)
}
} else {
window.open(htmlHref)
}
}
},
},
}),
]
}

}

/** Strike is currently unsupported by prosemirror-markdown */
Expand Down
Loading

0 comments on commit 69690ff

Please sign in to comment.