623 lines
16 KiB
Vue
623 lines
16 KiB
Vue
<template>
|
|
<div class="freeleaps-editor">
|
|
<div
|
|
class="editor-body"
|
|
:contenteditable="!disabled"
|
|
spellcheck="false"
|
|
ref="editor"
|
|
v-html="content"
|
|
@blur="updateAction"
|
|
@mousedown.stop="selectionStart"
|
|
@mouseup.stop="selectionEnd"
|
|
/>
|
|
<div v-if="!disabled" class="editor-control" :style="editorCtrlStyle">
|
|
<transition-group appear name="fade-transform" mode="out-in">
|
|
<template v-if="!inputing">
|
|
<div v-for="(item, index) in iconList" :key="index" class="editor-item">
|
|
<button
|
|
class="item-icon"
|
|
:class="{
|
|
activity: commandStates.indexOf(item.type) !== -1,
|
|
last: index === iconList.length - 1
|
|
}"
|
|
:data-info="item.name"
|
|
@click="iconClick($event, item.type)"
|
|
:data-bs-toggle="item.drop ? 'dropdown' : ''"
|
|
data-bs-auto-close="outside"
|
|
aria-expanded="false"
|
|
>
|
|
<svg-icon :icon="item.icon" class-name="icon" />
|
|
</button>
|
|
<div class="dropmenu drop-style" v-if="item.type === 'style'">
|
|
<ul>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'p', 'style')"><p>Text</p></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'pre', 'style')"><pre>code</pre></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'blockquote', 'style')"
|
|
><blockquote>引用</blockquote></a
|
|
>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'h1', 'style')"><h1>Caption 1</h1></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'h2', 'style')"><h2>Caption 2</h2></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'h3', 'style')"><h3>Caption 3</h3></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'h4', 'style')"><h4>Caption 4</h4></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'h5', 'style')"><h5>Caption 5</h5></a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'h6', 'style')"><h6>Caption 6</h6></a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="dropmenu drop-style" v-if="item.type === 'alignjustify'">
|
|
<ul>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'justifyCenter', 'alignjustify')">
|
|
<i class="iconfont icon-aligncenter"></i>
|
|
<span>middle</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'justifyLeft', 'alignjustify')">
|
|
<i class="iconfont icon-alignleft"></i>
|
|
<span>left aligned</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'justifyRight', 'alignjustify')">
|
|
<i class="iconfont icon-alignright"></i>
|
|
<span>right aligned</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click="iconClick($event, 'justifyFull', 'alignjustify')">
|
|
<i class="iconfont icon-alignjustify"></i>
|
|
<span>default aligned</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-if="inputing">
|
|
<div class="editor-item">
|
|
<button class="item-icon item-icon-back" @click="backAction" data-info="back">
|
|
<svg-icon icon="fe-back" class-name="icon" />
|
|
</button>
|
|
</div>
|
|
<div class="editor-item">
|
|
<input type="text" v-model="linkUrl" autofocus @keydown.enter="inputAction">
|
|
<svg-icon icon="msg-enter" class-name="item-enter-icon" />
|
|
</div>
|
|
</template>
|
|
</transition-group>
|
|
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import SvgIcon from '@/components/SvgIcon.vue'
|
|
import { websiteValidator } from '@/utils/validator'
|
|
export default {
|
|
name: 'FreeleapsEditor',
|
|
props: {
|
|
content: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
emits: ['update:content'],
|
|
components: { SvgIcon },
|
|
data() {
|
|
return {
|
|
selectedRange: '',
|
|
editorCtrlStyle: {},
|
|
commandStates: [],
|
|
inputing: false,
|
|
linkUrl: '',
|
|
iconList: [
|
|
// {
|
|
// name: 'lable', // hover name
|
|
// type: 'style', // click event handler
|
|
// icon: 'fe-paragraph', // icon style
|
|
// drop: true, // If there is drop menu
|
|
// canChoose: true, // chosen or not
|
|
// },
|
|
{
|
|
name: 'bold',
|
|
type: 'bold',
|
|
icon: 'fe-bold',
|
|
drop: false,
|
|
canChoose: true
|
|
},
|
|
{
|
|
name: 'italic',
|
|
type: 'italic',
|
|
icon: 'fe-italic',
|
|
drop: false,
|
|
canChoose: true
|
|
},
|
|
{
|
|
name: 'underline',
|
|
type: 'underline',
|
|
icon: 'fe-underline',
|
|
drop: false,
|
|
canChoose: true
|
|
},
|
|
// {
|
|
// name: 'strike',
|
|
// type: 'strike',
|
|
// icon: 'fe-strike',
|
|
// drop: false,
|
|
// canChoose: true,
|
|
// },
|
|
// {
|
|
// name: 'clear-format',
|
|
// type: 'clear',
|
|
// icon: 'fe-clear',
|
|
// drop: false,
|
|
// canChoose: false,
|
|
// },
|
|
// {
|
|
// name: 'font-color',
|
|
// type: 'fontFamily',
|
|
// icon: 'fe-char',
|
|
// drop: false,
|
|
// canChoose: true,
|
|
// },
|
|
{
|
|
name: 'unordered-list',
|
|
type: 'unorderedlist',
|
|
icon: 'fe-unorderedlist',
|
|
drop: false,
|
|
canChoose: true
|
|
},
|
|
{
|
|
name: 'ordered list',
|
|
type: 'orderedlist',
|
|
icon: 'fe-orderedlist',
|
|
drop: false,
|
|
canChoose: true
|
|
},
|
|
{
|
|
name: 'link',
|
|
type: 'link',
|
|
icon: 'fe-link',
|
|
drop: false,
|
|
canChoose: true
|
|
},
|
|
// {
|
|
// name: 'align-justify',
|
|
// type: 'alignjustify',
|
|
// icon: 'fe-alignjustify',
|
|
// drop: true,
|
|
// canChoose: true,
|
|
// }
|
|
]
|
|
}
|
|
},
|
|
methods: {
|
|
selectionStart() {
|
|
this.$refs.editor.blur()
|
|
},
|
|
selectionEnd(e) {
|
|
const sel = window.getSelection()
|
|
if (sel && sel.type === 'Range') {
|
|
// this.selectedRange = sel.getRangeAt(0)
|
|
// this.restoreSelection()
|
|
// console.log('window.getSelection',this.selectedRange)
|
|
// console.log('is bold ? ', this.queryCommandState(sel.getRangeAt(0), 'bold'))
|
|
// this.iconClick(null, 'bold')
|
|
this.editorCtrlStyle = { display: 'flex', top: `${e.layerY}px`, left: `${e.layerX}px` }
|
|
} else {
|
|
this.selectedRange = null
|
|
this.editorCtrlStyle = { display: 'none' }
|
|
this.inputing = false
|
|
this.linkUrl = ''
|
|
// this.commandStates = []
|
|
}
|
|
},
|
|
queryCommandState(range, command) {
|
|
const container = document.createElement('span')
|
|
container.style.display = 'none'
|
|
|
|
container.appendChild(range.cloneContents())
|
|
console.log('queryCommandState', container)
|
|
// range
|
|
document.body.appendChild(container)
|
|
const state = document.queryCommandState(command)
|
|
console.log('queryCommandIndeterm ', document.queryCommandIndeterm('bold'))
|
|
console.log('queryCommandState ', document.queryCommandState('bold'))
|
|
// range.collapse(true);
|
|
container.remove()
|
|
return state
|
|
},
|
|
iconClick(event, type, dropType) {
|
|
event.preventDefault()
|
|
this.$refs.editor.focus()
|
|
this.selectedRange = this.getSelect()
|
|
this.restoreSelection()
|
|
if (type === 'link') {
|
|
this.inputing = true
|
|
return
|
|
}
|
|
this.changeStyle(type)
|
|
this.$nextTick(() => {
|
|
if (dropType) {
|
|
type = dropType
|
|
}
|
|
const i = this.commandStates.indexOf(type)
|
|
if (i === -1) {
|
|
this.commandStates.push(type)
|
|
} else {
|
|
this.commandStates.splice(i, 1)
|
|
}
|
|
})
|
|
},
|
|
backAction() {
|
|
this.inputing = false
|
|
this.linkUrl = ''
|
|
this.$refs.editor.focus()
|
|
this.restoreSelection()
|
|
},
|
|
inputAction() {
|
|
const error = websiteValidator.validate(this.linkUrl)
|
|
if (error) {
|
|
alert(error)
|
|
return
|
|
}
|
|
const a_node = document.createElement('a')
|
|
a_node.setAttribute('href', this.linkUrl)
|
|
this.selectedRange.surroundContents(a_node)
|
|
this.backAction()
|
|
this.$refs.editor.blur()
|
|
this.linkUrl = ''
|
|
this.editorCtrlStyle = { display: 'none' }
|
|
},
|
|
getSelect() {
|
|
if (window.getSelection) {
|
|
let sel = window.getSelection()
|
|
if (sel.rangeCount > 0) {
|
|
return sel.getRangeAt(0)
|
|
}
|
|
} else if (document.selection) {
|
|
return document.selection.createRange()
|
|
}
|
|
return null
|
|
},
|
|
// change the selection's style
|
|
changeStyle(type) {
|
|
switch (type) {
|
|
case 'bold':
|
|
document.execCommand('bold', false)
|
|
break
|
|
case 'underline':
|
|
document.execCommand('underline', false)
|
|
break
|
|
case 'strike':
|
|
document.execCommand('strikeThrough', false)
|
|
break
|
|
case 'italic':
|
|
document.execCommand('italic', false)
|
|
break
|
|
case 'clear':
|
|
document.execCommand('removeFormat', false)
|
|
break
|
|
case 'unorderedlist':
|
|
document.execCommand('insertUnorderedList', false)
|
|
break
|
|
case 'orderedlist':
|
|
document.execCommand('insertorderedList', false)
|
|
break
|
|
case 'h1':
|
|
case 'h2':
|
|
case 'h3':
|
|
case 'h4':
|
|
case 'h5':
|
|
case 'h6':
|
|
case 'p':
|
|
case 'pre':
|
|
case 'blockquote':
|
|
document.execCommand('formatBlock', false, type)
|
|
break
|
|
case 'justifyCenter':
|
|
case 'justifyFull':
|
|
case 'justifyLeft':
|
|
case 'justifyRight':
|
|
document.execCommand(type, false)
|
|
break
|
|
default:
|
|
console.log('none')
|
|
}
|
|
this.$refs.editor.blur()
|
|
// window.getSelection().removeAllRanges()
|
|
},
|
|
restoreSelection() {
|
|
let selection = window.getSelection()
|
|
if (this.selectedRange) {
|
|
try {
|
|
selection.removeAllRanges()
|
|
} catch (ex) {
|
|
document.body.createTextRange().select()
|
|
document.selection.empty()
|
|
}
|
|
selection.addRange(this.selectedRange)
|
|
}
|
|
},
|
|
|
|
updateAction($event) {
|
|
let html = $event.target.innerHTML || ''
|
|
this.$emit('update:content', html)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.freeleaps-editor {
|
|
position: relative;
|
|
min-width: 100px;
|
|
background-color: #fff;
|
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6,
|
|
.h1,
|
|
.h2,
|
|
.h3,
|
|
.h4,
|
|
.h5,
|
|
.h6,
|
|
p,
|
|
a {
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
line-height: 1.1;
|
|
color: inherit;
|
|
margin: 0;
|
|
text-decoration: none;
|
|
}
|
|
code,
|
|
kbd,
|
|
pre,
|
|
samp {
|
|
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
}
|
|
.editor-control {
|
|
display: none;
|
|
width: fit-content;
|
|
height: 26px;
|
|
background-color: #f8f8f9;
|
|
position: absolute;
|
|
}
|
|
.dropmenu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 32px;
|
|
left: 0;
|
|
min-width: 160px;
|
|
padding: 5px 0;
|
|
margin: 2px 0 0;
|
|
font-size: 14px;
|
|
text-align: left;
|
|
list-style: none;
|
|
background-color: #fff;
|
|
background-clip: padding-box;
|
|
border: 1px solid #ccc;
|
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
border-radius: 4px;
|
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
|
ul {
|
|
padding: 10px;
|
|
margin-bottom: 0;
|
|
}
|
|
ul li {
|
|
text-align: left;
|
|
list-style: none;
|
|
a {
|
|
display: block;
|
|
padding: 5px 10px;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
}
|
|
.drop-align {
|
|
min-width: 100px;
|
|
}
|
|
.editor-body {
|
|
height: 300px;
|
|
padding: 10px;
|
|
color: #000;
|
|
background-color: #fff;
|
|
overflow: auto;
|
|
outline: none;
|
|
text-align: left;
|
|
border-radius: 4px;
|
|
position: relative;
|
|
p {
|
|
font-size: 14px;
|
|
color: #68747f;
|
|
margin: 0 0 10px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.editor-item {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
user-select: none;
|
|
cursor: pointer;
|
|
|
|
input {
|
|
height: 24px;
|
|
border: 1px solid #e1e1e1;
|
|
box-shadow: none;
|
|
outline: none;
|
|
padding-left: 5px;
|
|
padding-right: 24px;
|
|
|
|
&:focus {
|
|
border-color: $primary;
|
|
}
|
|
}
|
|
|
|
.item-enter-icon {
|
|
position: absolute;
|
|
right: 2px;
|
|
width: 16px;
|
|
height: 16px;
|
|
padding: 3px;
|
|
background-color: #e1e1e1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.item-icon {
|
|
position: relative;
|
|
width: 50px;
|
|
height: 16px;
|
|
font-size: 16px;
|
|
color: #9ea2af;
|
|
font-weight: normal;
|
|
white-space: nowrap;
|
|
vertical-align: middle;
|
|
touch-action: manipulation;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
outline: none;
|
|
transition: all 0.1s ease-out;
|
|
border: none;
|
|
line-height: 16px;
|
|
background: transparent;
|
|
border-right: 1px solid #e7e8eb;
|
|
&.last {
|
|
border-right: 0;
|
|
}
|
|
&::after {
|
|
position: absolute;
|
|
top: 0;
|
|
content: attr(data-info);
|
|
top: 40px;
|
|
left: 20px;
|
|
padding: 5px 8px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
line-height: 1.5;
|
|
font-size: 13px;
|
|
color: #fff;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
-webkit-transform: translateX(-50%);
|
|
transform: translateX(-50%);
|
|
visibility: hidden;
|
|
opacity: 0.9;
|
|
letter-spacing: 1px;
|
|
z-index: 9999;
|
|
}
|
|
&::before {
|
|
position: absolute;
|
|
content: '';
|
|
top: 35px;
|
|
left: 20px;
|
|
width: 0;
|
|
height: 0;
|
|
margin: 0 0 0 -6px;
|
|
font-size: 0;
|
|
color: rgba(0, 0, 0, 0.8);
|
|
border-bottom: 6px solid currentColor;
|
|
border-left: 6px solid transparent;
|
|
border-right: 6px solid transparent;
|
|
visibility: hidden;
|
|
opacity: 0.9;
|
|
z-index: 9999;
|
|
}
|
|
&:hover,
|
|
&.activity {
|
|
color: black;
|
|
}
|
|
&:hover:after,
|
|
&:hover:before {
|
|
visibility: visible;
|
|
}
|
|
&.activity + .dropmenu {
|
|
display: block;
|
|
}
|
|
&.item-icon-back {
|
|
color: black;
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
<style>
|
|
.freeleaps-editor blockquote {
|
|
padding: 10px 20px;
|
|
font-size: 17.5px;
|
|
border-left: 5px solid #f86466;
|
|
background: white;
|
|
}
|
|
.freeleaps-editor pre {
|
|
display: block;
|
|
padding: 9.5px;
|
|
font-size: 13px;
|
|
line-height: 1.42857143;
|
|
color: #333;
|
|
word-break: break-all;
|
|
word-wrap: break-word;
|
|
background-color: #f5f5f5;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
}
|
|
.editor-body blockquote {
|
|
margin-bottom: 30px;
|
|
}
|
|
.editor-body pre {
|
|
margin-bottom: 10px;
|
|
}
|
|
.editor-body ul {
|
|
padding-left: 40px;
|
|
list-style-type: disc;
|
|
}
|
|
.editor-body ol {
|
|
padding-left: 40px;
|
|
}
|
|
|
|
/* fade-transform */
|
|
.fade-transform-leave-active,
|
|
.fade-transform-enter-active {
|
|
transition: all 300;
|
|
}
|
|
|
|
.fade-transform-enter-from {
|
|
opacity: 0;
|
|
transform: translateX(-50px);
|
|
}
|
|
|
|
.fade-transform-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(50px);
|
|
}
|
|
.fade-transform-active {
|
|
position: absolute;
|
|
}
|
|
</style>
|