|
|
@@ -139,6 +139,7 @@ import {
|
|
|
import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
|
|
|
import { getTagList } from '@/api/dimension.js'
|
|
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
|
+import { DomEditor, SlateTransforms, SlatePath } from '@wangeditor/editor'
|
|
|
import '@wangeditor/editor/dist/css/style.css'
|
|
|
|
|
|
export default {
|
|
|
@@ -257,6 +258,11 @@ export default {
|
|
|
const baseURL = process.env.VUE_APP_BASE_API || ''
|
|
|
const token = localStorage.getItem('token')
|
|
|
this.editorConfig = {
|
|
|
+ hoverbarKeys: {
|
|
|
+ video: {
|
|
|
+ menuKeys: []
|
|
|
+ }
|
|
|
+ },
|
|
|
MENU_CONF: {
|
|
|
uploadImage: {
|
|
|
server: baseURL + '/api/admin/articles/upload/image',
|
|
|
@@ -310,13 +316,14 @@ onError: (file, err, res) => {
|
|
|
},
|
|
|
handleEditorCreated(editor) {
|
|
|
this.editorInstance = editor
|
|
|
- this.bindVideoFullscreen(editor)
|
|
|
+ this.bindVideoInteractions(editor)
|
|
|
},
|
|
|
- bindVideoFullscreen(editor) {
|
|
|
+ bindVideoInteractions(editor) {
|
|
|
const root = (editor && editor.getEditableContainer && editor.getEditableContainer())
|
|
|
|| (this.$el && this.$el.querySelector('.w-e-text-container'))
|
|
|
if (!root || this._videoFsBound) return
|
|
|
this._videoFsBound = true
|
|
|
+ this._videoEditorRoot = root
|
|
|
this._onVideoDblClick = (e) => {
|
|
|
const video = this.findEventVideo(e)
|
|
|
if (!video) return
|
|
|
@@ -324,9 +331,11 @@ onError: (file, err, res) => {
|
|
|
this.enterVideoFullscreen(video)
|
|
|
}
|
|
|
this._onVideoFsClick = (e) => {
|
|
|
- const box = e.target && e.target.closest && (
|
|
|
- e.target.closest('.w-e-textarea-video-container') || e.target.closest('[data-w-e-type="video"]')
|
|
|
- )
|
|
|
+ if (this._justDraggedVideo) {
|
|
|
+ this._justDraggedVideo = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const box = this.findVideoBox(e.target)
|
|
|
if (!box) return
|
|
|
const rect = box.getBoundingClientRect()
|
|
|
const x = e.clientX - rect.left
|
|
|
@@ -338,8 +347,24 @@ onError: (file, err, res) => {
|
|
|
e.stopPropagation()
|
|
|
this.enterVideoFullscreen(video)
|
|
|
}
|
|
|
+ this._onVideoMouseDown = (e) => {
|
|
|
+ this.onVideoDragMouseDown(editor, e)
|
|
|
+ }
|
|
|
+ this._onVideoMouseMove = (e) => {
|
|
|
+ this.onVideoDragMouseMove(editor, e)
|
|
|
+ }
|
|
|
+ this._onVideoMouseUp = (e) => {
|
|
|
+ this.onVideoDragMouseUp(editor, e)
|
|
|
+ }
|
|
|
root.addEventListener('dblclick', this._onVideoDblClick)
|
|
|
root.addEventListener('click', this._onVideoFsClick, true)
|
|
|
+ root.addEventListener('mousedown', this._onVideoMouseDown, true)
|
|
|
+ document.addEventListener('mousemove', this._onVideoMouseMove)
|
|
|
+ document.addEventListener('mouseup', this._onVideoMouseUp)
|
|
|
+ },
|
|
|
+ findVideoBox(el) {
|
|
|
+ if (!el || !el.closest) return null
|
|
|
+ return el.closest('.w-e-textarea-video-container') || el.closest('[data-w-e-type="video"]')
|
|
|
},
|
|
|
findEventVideo(e) {
|
|
|
const t = e && e.target
|
|
|
@@ -347,16 +372,189 @@ onError: (file, err, res) => {
|
|
|
if (t.tagName === 'VIDEO') return t
|
|
|
return t.closest ? t.closest('video') : null
|
|
|
},
|
|
|
+ isVideoDragIgnorePoint(box, clientX, clientY) {
|
|
|
+ const rect = box.getBoundingClientRect()
|
|
|
+ const x = clientX - rect.left
|
|
|
+ const y = clientY - rect.top
|
|
|
+ if (x >= rect.width - 36 && y <= 36) return true
|
|
|
+ if (y > rect.height - 44) return true
|
|
|
+ return false
|
|
|
+ },
|
|
|
+ onVideoDragMouseDown(editor, e) {
|
|
|
+ if (this.readonly || (editor && editor.isDisabled && editor.isDisabled())) return
|
|
|
+ if (e.button !== 0) return
|
|
|
+ const box = this.findVideoBox(e.target)
|
|
|
+ if (!box) return
|
|
|
+ if (this.isVideoDragIgnorePoint(box, e.clientX, e.clientY)) return
|
|
|
+ this._videoDragState = {
|
|
|
+ startX: e.clientX,
|
|
|
+ startY: e.clientY,
|
|
|
+ box: box,
|
|
|
+ dragging: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onVideoDragMouseMove(editor, e) {
|
|
|
+ const state = this._videoDragState
|
|
|
+ if (!state) return
|
|
|
+ const dx = e.clientX - state.startX
|
|
|
+ const dy = e.clientY - state.startY
|
|
|
+ if (!state.dragging && (dx * dx + dy * dy) < 36) return
|
|
|
+ if (!state.dragging) {
|
|
|
+ state.dragging = true
|
|
|
+ state.box.classList.add('is-video-dragging')
|
|
|
+ const wrap = this.$el && this.$el.querySelector('.rich-editor')
|
|
|
+ if (wrap) wrap.classList.add('is-dragging-video')
|
|
|
+ }
|
|
|
+ e.preventDefault()
|
|
|
+ this.updateVideoDropIndicator(editor, e)
|
|
|
+ },
|
|
|
+ onVideoDragMouseUp(editor, e) {
|
|
|
+ const state = this._videoDragState
|
|
|
+ this._videoDragState = null
|
|
|
+ const wrap = this.$el && this.$el.querySelector('.rich-editor')
|
|
|
+ if (wrap) wrap.classList.remove('is-dragging-video')
|
|
|
+ this.clearVideoDropIndicator()
|
|
|
+ if (!state) return
|
|
|
+ if (state.box) state.box.classList.remove('is-video-dragging')
|
|
|
+ if (!state.dragging) return
|
|
|
+ this._justDraggedVideo = true
|
|
|
+ this.moveVideoToEvent(editor, state.box, e)
|
|
|
+ },
|
|
|
+ updateVideoDropIndicator(editor, e) {
|
|
|
+ const root = this.getVideoScrollRoot()
|
|
|
+ if (!root) return
|
|
|
+ const info = this.getVideoDropInfo(editor, e)
|
|
|
+ if (!info) {
|
|
|
+ this.clearVideoDropIndicator()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let line = this._videoDropLine
|
|
|
+ if (!line) {
|
|
|
+ line = document.createElement('div')
|
|
|
+ line.className = 'article-video-drop-line'
|
|
|
+ root.appendChild(line)
|
|
|
+ this._videoDropLine = line
|
|
|
+ } else if (line.parentNode !== root) {
|
|
|
+ root.appendChild(line)
|
|
|
+ }
|
|
|
+ const rootRect = root.getBoundingClientRect()
|
|
|
+ line.style.top = (info.y - rootRect.top + root.scrollTop) + 'px'
|
|
|
+ line.style.display = 'block'
|
|
|
+ },
|
|
|
+ getVideoScrollRoot() {
|
|
|
+ const root = this._videoEditorRoot
|
|
|
+ if (!root) return null
|
|
|
+ return root.querySelector('.w-e-scroll') || root
|
|
|
+ },
|
|
|
+ clearVideoDropIndicator() {
|
|
|
+ if (this._videoDropLine) {
|
|
|
+ this._videoDropLine.style.display = 'none'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getVideoDropInfo(editor, e) {
|
|
|
+ if (!editor) return null
|
|
|
+ const root = this.getVideoScrollRoot()
|
|
|
+ if (!root) return null
|
|
|
+ const rootRect = root.getBoundingClientRect()
|
|
|
+ if (e.clientY < rootRect.top || e.clientY > rootRect.bottom) return null
|
|
|
+ let path = null
|
|
|
+ let targetDom = null
|
|
|
+ try {
|
|
|
+ const range = DomEditor.findEventRange(editor, e)
|
|
|
+ if (range && range.anchor && range.anchor.path) {
|
|
|
+ path = range.anchor.path
|
|
|
+ }
|
|
|
+ } catch (err) {}
|
|
|
+ if (!path) {
|
|
|
+ const hit = document.elementFromPoint(e.clientX, e.clientY)
|
|
|
+ let cur = hit
|
|
|
+ while (cur && cur !== root) {
|
|
|
+ try {
|
|
|
+ const node = DomEditor.toSlateNode(editor, cur)
|
|
|
+ if (node && node.type) {
|
|
|
+ path = DomEditor.findPath(editor, node)
|
|
|
+ targetDom = cur
|
|
|
+ break
|
|
|
+ }
|
|
|
+ } catch (err) {}
|
|
|
+ cur = cur.parentNode
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!path || !path.length) {
|
|
|
+ const last = editor.children && editor.children.length ? editor.children[editor.children.length - 1] : null
|
|
|
+ if (!last) return { toPath: [0], y: rootRect.top + 8 }
|
|
|
+ try {
|
|
|
+ targetDom = editor.toDOMNode(last)
|
|
|
+ } catch (err) {
|
|
|
+ return { toPath: [editor.children.length], y: rootRect.bottom - 8 }
|
|
|
+ }
|
|
|
+ const rect = targetDom.getBoundingClientRect()
|
|
|
+ return { toPath: [editor.children.length], y: rect.bottom }
|
|
|
+ }
|
|
|
+ const topIndex = path[0]
|
|
|
+ const block = editor.children[topIndex]
|
|
|
+ if (!targetDom && block && editor.toDOMNode) {
|
|
|
+ try {
|
|
|
+ targetDom = editor.toDOMNode(block)
|
|
|
+ } catch (err) {}
|
|
|
+ }
|
|
|
+ if (!targetDom) return { toPath: [topIndex], y: e.clientY }
|
|
|
+ const rect = targetDom.getBoundingClientRect()
|
|
|
+ const insertAfter = e.clientY > rect.top + rect.height / 2
|
|
|
+ return {
|
|
|
+ toPath: [insertAfter ? topIndex + 1 : topIndex],
|
|
|
+ y: insertAfter ? rect.bottom : rect.top
|
|
|
+ }
|
|
|
+ },
|
|
|
+ moveVideoToEvent(editor, box, e) {
|
|
|
+ if (!editor || !box) return
|
|
|
+ let videoNode = null
|
|
|
+ try {
|
|
|
+ videoNode = DomEditor.toSlateNode(editor, box)
|
|
|
+ } catch (err) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!videoNode || videoNode.type !== 'video') return
|
|
|
+ let fromPath = null
|
|
|
+ try {
|
|
|
+ fromPath = DomEditor.findPath(editor, videoNode)
|
|
|
+ } catch (err) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const info = this.getVideoDropInfo(editor, e)
|
|
|
+ if (!info || !info.toPath) return
|
|
|
+ let toPath = info.toPath.slice()
|
|
|
+ const max = editor.children ? editor.children.length : 0
|
|
|
+ if (toPath[0] > max) toPath[0] = max
|
|
|
+ if (SlatePath.equals(fromPath, toPath)) return
|
|
|
+ if (fromPath.length === 1 && toPath.length === 1 && toPath[0] === fromPath[0] + 1) return
|
|
|
+ try {
|
|
|
+ SlateTransforms.moveNodes(editor, { at: fromPath, to: toPath })
|
|
|
+ } catch (err) {
|
|
|
+ console.error('移动视频失败', err)
|
|
|
+ }
|
|
|
+ },
|
|
|
enterVideoFullscreen(video) {
|
|
|
if (!video) return
|
|
|
const req = video.requestFullscreen || video.webkitRequestFullscreen || video.msRequestFullscreen
|
|
|
if (req) req.call(video)
|
|
|
},
|
|
|
unbindVideoFullscreen() {
|
|
|
- const root = (this.editorInstance && this.editorInstance.getEditableContainer && this.editorInstance.getEditableContainer())
|
|
|
+ const root = this._videoEditorRoot
|
|
|
+ || (this.editorInstance && this.editorInstance.getEditableContainer && this.editorInstance.getEditableContainer())
|
|
|
|| (this.$el && this.$el.querySelector('.w-e-text-container'))
|
|
|
if (root && this._onVideoDblClick) root.removeEventListener('dblclick', this._onVideoDblClick)
|
|
|
if (root && this._onVideoFsClick) root.removeEventListener('click', this._onVideoFsClick, true)
|
|
|
+ if (root && this._onVideoMouseDown) root.removeEventListener('mousedown', this._onVideoMouseDown, true)
|
|
|
+ if (this._onVideoMouseMove) document.removeEventListener('mousemove', this._onVideoMouseMove)
|
|
|
+ if (this._onVideoMouseUp) document.removeEventListener('mouseup', this._onVideoMouseUp)
|
|
|
+ this.clearVideoDropIndicator()
|
|
|
+ if (this._videoDropLine && this._videoDropLine.parentNode) {
|
|
|
+ this._videoDropLine.parentNode.removeChild(this._videoDropLine)
|
|
|
+ }
|
|
|
+ this._videoDropLine = null
|
|
|
+ this._videoEditorRoot = null
|
|
|
+ this._videoDragState = null
|
|
|
this._videoFsBound = false
|
|
|
},
|
|
|
async loadCategories() {
|
|
|
@@ -658,10 +856,14 @@ onError: (file, err, res) => {
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
.rich-editor /deep/ .w-e-text-container {
|
|
|
+ position: relative;
|
|
|
min-height: 400px;
|
|
|
max-height: 600px;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
+.rich-editor /deep/ .w-e-scroll {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
.rich-editor /deep/ .w-e-textarea-video-container,
|
|
|
.rich-editor /deep/ [data-w-e-type="video"] {
|
|
|
position: relative;
|
|
|
@@ -676,6 +878,15 @@ onError: (file, err, res) => {
|
|
|
overflow: hidden;
|
|
|
line-height: 0;
|
|
|
text-align: left;
|
|
|
+ cursor: grab;
|
|
|
+}
|
|
|
+.rich-editor /deep/ .w-e-textarea-video-container.is-video-dragging,
|
|
|
+.rich-editor /deep/ [data-w-e-type="video"].is-video-dragging {
|
|
|
+ opacity: 0.45;
|
|
|
+ cursor: grabbing;
|
|
|
+}
|
|
|
+.rich-editor.is-dragging-video {
|
|
|
+ cursor: grabbing;
|
|
|
}
|
|
|
.rich-editor /deep/ .w-e-textarea-video-container video,
|
|
|
.rich-editor /deep/ [data-w-e-type="video"] video {
|
|
|
@@ -685,7 +896,18 @@ onError: (file, err, res) => {
|
|
|
height: auto;
|
|
|
background: #000;
|
|
|
object-fit: contain;
|
|
|
- cursor: zoom-in;
|
|
|
+ cursor: grab;
|
|
|
+}
|
|
|
+.rich-editor /deep/ .article-video-drop-line {
|
|
|
+ position: absolute;
|
|
|
+ left: 10px;
|
|
|
+ right: 10px;
|
|
|
+ height: 3px;
|
|
|
+ background: #409eff;
|
|
|
+ border-radius: 2px;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 60;
|
|
|
+ display: none;
|
|
|
}
|
|
|
.rich-editor /deep/ .w-e-textarea-video-container::after,
|
|
|
.rich-editor /deep/ [data-w-e-type="video"]::after {
|