Merge pull request #2275 from dataease/pr@dev@feat_fulltext

feat: 新增富文本编辑器
This commit is contained in:
王嘉豪 2022-05-19 14:01:12 +08:00 committed by GitHub
commit 1cab504402
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 147 additions and 287 deletions

View File

@ -4,11 +4,7 @@
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.4;
margin: 1rem;
}
table {
border-collapse: collapse;

View File

@ -661,7 +661,6 @@ export default {
this.maxH = val
},
w(val) {
if (this.resizing || this.dragging) {
return
}
@ -760,7 +759,7 @@ export default {
elementMouseDown(e) {
// private
this.$store.commit('setClickComponentStatus', true)
if (this.element.component !== 'v-text' && this.element.component !== 'rect-shape' && this.element.component !== 'de-input-search' && this.element.component !== 'de-select-grid' && this.element.component !== 'de-number-range' && this.element.component !== 'de-date') {
if (this.element.component !== 'v-text' && this.element.component !== 'de-rich-text' && this.element.component !== 'rect-shape' && this.element.component !== 'de-input-search' && this.element.component !== 'de-select-grid' && this.element.component !== 'de-number-range' && this.element.component !== 'de-date') {
e.preventDefault()
}
//
@ -1003,7 +1002,7 @@ export default {
move(e) {
if (this.resizing) {
this.handleResize(e)
} else if (this.dragging) {
} else if (this.dragging && !this.element.editing) {
this.handleDrag(e)
} else if (this.rotating) {
this.handleRotate(e)

View File

@ -58,7 +58,7 @@ export default {
//
if (this.curComponent.type === 'v-text' || this.curComponent.type === 'rect-shape') {
if (this.curComponent.type === 'v-text' || this.curComponent.type === 'de-rich-text' || this.curComponent.type === 'rect-shape') {
bus.$emit('component-dialog-style')
}
},

View File

@ -217,7 +217,7 @@ export default {
edit() {
if (this.curComponent.type === 'custom') {
bus.$emit('component-dialog-edit')
} else if (this.curComponent.type === 'v-text' || this.curComponent.type === 'rect-shape') {
} else if (this.curComponent.type === 'v-text' || this.curComponent.type === 'de-rich-text' || this.curComponent.type === 'rect-shape') {
bus.$emit('component-dialog-style')
} else { bus.$emit('change_panel_right_draw', true) }
},

View File

@ -67,7 +67,7 @@ export default {
edit() {
if (this.curComponent.type === 'custom') {
bus.$emit('component-dialog-edit')
} else if (this.curComponent.type === 'v-text' || this.curComponent.type === 'rect-shape') {
} else if (this.curComponent.type === 'v-text' || this.curComponent.type === 'de-rich-text' || this.curComponent.type === 'rect-shape') {
bus.$emit('component-dialog-style')
} else { bus.$emit('change_panel_right_draw', true) }
},

View File

@ -61,22 +61,8 @@
@canvasDragging="canvasDragging"
@editComponent="editComponent(index,item)"
>
<component
:is="item.component"
v-if="renderOk&&item.type==='v-text'"
:id="'component' + item.id"
ref="wrapperChild"
class="component"
:style="getComponentStyleDefault(item.style)"
:prop-value="item.propValue"
:element="item"
:out-style="getShapeStyleInt(item.style)"
:edit-mode="'edit'"
:active="item === curComponent"
@input="handleInput"
/>
<de-out-widget
v-else-if="renderOk&&item.type==='custom'"
v-if="renderOk&&item.type==='custom'"
:id="'component' + item.id"
ref="wrapperChild"
class="component"
@ -113,6 +99,7 @@
:active="item === curComponent"
:edit-mode="'edit'"
:h="getShapeStyleIntDeDrag(item.style,'height')"
@input="handleInput"
/>
</de-drag>
<!--拖拽阴影部分-->

View File

@ -1,13 +1,20 @@
<template>
<div class="tinymce-editor" style="background-color: #8a8b8d!important;">
<div v-if="editStatus" class="rich-main-class">
<Editor
v-show="canEdit"
:id="tinymceId"
v-model="myValue"
style="width: 100%;height: 100%"
:init="init"
:disabled="disabled"
@onClick="onClick"
/>
<div v-show="!canEdit" style="width: 100%;height: 100%" @dblclick="setEdit" v-html="myValue" />
</div>
<div v-else class="rich-main-class">
<div v-html="myValue" />
</div>
</template>
<script>
@ -24,6 +31,8 @@ import 'tinymce/plugins/lists' // 列表插件
import 'tinymce/plugins/charmap' //
import 'tinymce/plugins/media' //
import 'tinymce/plugins/wordcount'//
import 'tinymce/plugins/table'//
import { mapState } from 'vuex'
// const fonts = [
// '=',
@ -54,37 +63,40 @@ import 'tinymce/plugins/wordcount'// 字数统计
// ]
export default {
name: 'DeRichText',
components: {
Editor
},
props: {
//
value: {
propValue: {
type: String,
default: ''
require: true
},
element: {
type: Object
},
editMode: {
type: String,
require: false,
default: 'preview'
},
active: {
type: Boolean,
require: false,
default: false
},
//
disabled: {
type: Boolean,
default: false
},
//
plugins: {
type: [String, Array],
default: 'advlist autolink link image lists charmap media wordcount'
},
//
toolbar: {
type: [String, Array],
// default: 'fontsizeselect bold italic alignleft aligncenter alignright forecolor backcolor link'
default: 'undo redo | fontsizeselect | bold italic forecolor backcolor| alignleft aligncenter alignright | link'
}
},
data() {
return {
canEdit: false,
//
tinymceId: 'tinymce',
myValue: this.value,
myValue: this.propValue,
init: {
selector: '#tinymce',
toolbar_items_size: 'small',
@ -92,31 +104,40 @@ export default {
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide', //
content_css: '/tinymce/skins/content/default/content.css',
plugins: this.plugins, //
plugins: 'advlist autolink link image lists charmap media wordcount table', //
//
toolbar: this.toolbar,
toolbar: 'undo redo | fontsizeselect | bold italic forecolor backcolor| alignleft aligncenter alignright | lists image media table link',
toolbar_location: '/',
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px', //
// font_formats: fonts.join(';'),
menubar: false,
// height: 500, //
placeholder: '在这里输入文字',
placeholder: '双击输入文字',
inline: true, //
branding: false
}
}
},
computed: {
editStatus() {
return this.editMode === 'edit' && !this.mobileLayoutStatus
},
...mapState([
'mobileLayoutStatus'
])
},
watch: {
//
value(newValue) {
active(val) {
if (!val) {
this.canEdit = false
}
},
//
propValue(newValue) {
this.myValue = (newValue == null ? '' : newValue)
},
myValue(newValue) {
if (this.triggerChange) {
this.$emit('change', newValue)
} else {
this.$emit('input', newValue)
}
this.element.propValue = newValue
this.$store.state.styleChangeTimes++
}
},
mounted() {
@ -126,10 +147,23 @@ export default {
onClick(e) {
this.$emit('onClick', e, tinymce)
},
//
clear() {
this.myValue = ''
setEdit() {
this.canEdit = true
this.element.editing = true
}
}
}
</script>
<style lang="scss" scoped>
.rich-main-class {
width: 100%;
height: 100%;
overflow-y: auto!important;
}
::-webkit-scrollbar {
width: 0px!important;
height: 0px!important;
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<TinyMCE v-model="curComponent.propValue" />
</template>
<script>
import TinyMCE from '@/components/TinyMCE/index.vue'
export default {
components: { TinyMCE },
data() {
return {
}
},
computed: {
curComponent() {
return this.$store.state.curComponent
}
}
}
</script>
<style lang="scss" scoped>
.attr-list {
overflow: auto;
padding: 20px;
padding-top: 0;
height: 100%;
}
</style>

View File

@ -1,150 +0,0 @@
<template>
<div v-if="editMode == 'edit'" class="v-text" @keydown="handleKeydown" @keyup="handleKeyup">
<!-- tabindex >= 0 使得双击时聚集该元素 -->
<div
ref="text"
:contenteditable="canEdit"
:class="{ canEdit }"
:tabindex="element.id"
:style="{ verticalAlign: element.style.verticalAlign }"
@dblclick="setEdit"
@paste="clearStyle"
@mousedown="handleMousedown"
@blur="handleBlur"
@input="handleInput"
v-html="element.propValue"
/>
</div>
<div v-else class="v-text">
<div :style="{ verticalAlign: element.style.verticalAlign }" v-html="element.propValue" />
</div>
</template>
<script>
import { keycodes } from '@/components/canvas/utils/shortcutKey.js'
export default {
props: {
// eslint-disable-next-line vue/require-default-prop
propValue: {
type: String,
require: true
},
// eslint-disable-next-line vue/require-default-prop
element: {
type: Object
},
editMode: {
type: String,
require: false,
default: 'preview'
},
active: {
type: Boolean,
require: false,
default: false
}
},
data() {
return {
canEdit: false,
ctrlKey: 17,
isCtrlDown: false
}
},
computed: {
},
watch: {
active: {
handler(newVal, oldVla) {
this.removeSelectText()
},
deep: true
}
},
methods: {
handleInput(e) {
this.$emit('input', this.element, e.target.innerHTML)
},
handleKeydown(e) {
if (e.keyCode === this.ctrlKey) {
this.isCtrlDown = true
} else if (this.isCtrlDown && this.canEdit && keycodes.includes(e.keyCode)) {
e.stopPropagation()
} else if (e.keyCode === 46) { // deleteKey
e.stopPropagation()
}
},
handleKeyup(e) {
if (e.keyCode === this.ctrlKey) {
this.isCtrlDown = false
}
},
handleMousedown(e) {
if (this.canEdit) {
e.stopPropagation()
}
},
clearStyle(e) {
e.preventDefault()
const clp = e.clipboardData
const text = clp.getData('text/plain') || ''
if (text !== '') {
document.execCommand('insertText', false, text)
}
this.$emit('input', this.element, e.target.innerHTML)
},
handleBlur(e) {
this.element.propValue = e.target.innerHTML || '&nbsp;'
this.canEdit = false
},
setEdit() {
this.canEdit = true
//
this.selectText(this.$refs.text)
},
selectText(element) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element)
selection.removeAllRanges()
selection.addRange(range)
},
removeSelectText() {
const selection = window.getSelection()
selection.removeAllRanges()
}
}
}
</script>
<style lang="scss" scoped>
.v-text {
width: 100%;
height: 100%;
display: table;
div {
display: table-cell;
width: 100%;
height: 100%;
outline: none;
}
.canEdit {
cursor: text;
height: 100%;
}
}
</style>

View File

@ -126,6 +126,14 @@ export const assistList = [
icon: 'iconfont icon-text',
defaultClass: 'text-filter'
},
{
id: '10002',
component: 'de-rich-text',
type: 'de-rich-text',
label: '富文本',
icon: 'iconfont icon-fuwenbenkuang',
defaultClass: 'text-filter'
},
{
id: '10004',
component: 'rect-shape',
@ -249,26 +257,28 @@ const list = [
},
{
id: '10002',
component: 'v-button',
label: '按钮',
propValue: '按钮',
icon: 'button',
type: 'v-button',
component: 'de-rich-text',
label: '富文本',
propValue: '双击输入文字',
icon: 'icon-fuwenbenkuang',
type: 'de-rich-text',
mobileStyle: BASE_MOBILE_STYLE,
hyperlinks: HYPERLINKS,
style: {
width: 100,
height: 34,
borderWidth: '',
borderColor: '',
borderRadius: '',
fontSize: 14,
width: 400,
height: 100,
fontSize: 22,
fontWeight: 400,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: '',
backgroundColor: ''
textAlign: 'center',
color: '#000000',
verticalAlign: 'middle'
},
x: 1,
y: 1,
sizex: 10,
sizey: 2,
miniSizex: 1,
miniSizey: 1
},

View File

@ -9,7 +9,8 @@ import UserView from '@/components/canvas/custom-component/UserView'
import DeVideo from '@/components/canvas/custom-component/DeVideo'
import DeFrame from '@/components/canvas/custom-component/DeFrame'
import DeStreamMedia from '@/components/canvas/custom-component/DeStreamMedia'
import DeRichText from '@/components/canvas/custom-component/DeRichText'
Vue.component('DeRichText', DeRichText)
Vue.component('DeStreamMedia', DeStreamMedia)
Vue.component('Picture', Picture)
Vue.component('VText', VText)

View File

@ -159,6 +159,12 @@ const data = {
dragging: false,
resizing: false
}
// Is the current component in editing status
if (!state.curComponent) {
component['editing'] = false
} else if (component.id !== state.curComponent.id) {
component['editing'] = false
}
}
state.styleChangeTimes = 0
state.curComponent = component

View File

@ -54,6 +54,12 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe670;</span>
<div class="name">富文本框</div>
<div class="code-name">&amp;#xe670;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe6e5;</span>
<div class="name">下架</div>
@ -588,9 +594,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1652670008819') format('woff2'),
url('iconfont.woff?t=1652670008819') format('woff'),
url('iconfont.ttf?t=1652670008819') format('truetype');
src: url('iconfont.woff2?t=1652937715816') format('woff2'),
url('iconfont.woff?t=1652937715816') format('woff'),
url('iconfont.ttf?t=1652937715816') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -616,6 +622,15 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-fuwenbenkuang"></span>
<div class="name">
富文本框
</div>
<div class="code-name">.icon-fuwenbenkuang
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-unpublish"></span>
<div class="name">
@ -1417,6 +1432,14 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-fuwenbenkuang"></use>
</svg>
<div class="name">富文本框</div>
<div class="code-name">#icon-fuwenbenkuang</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-unpublish"></use>

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2459092 */
src: url('iconfont.woff2?t=1652670008819') format('woff2'),
url('iconfont.woff?t=1652670008819') format('woff'),
url('iconfont.ttf?t=1652670008819') format('truetype');
src: url('iconfont.woff2?t=1652937715816') format('woff2'),
url('iconfont.woff?t=1652937715816') format('woff'),
url('iconfont.ttf?t=1652937715816') format('truetype');
}
.iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-fuwenbenkuang:before {
content: "\e670";
}
.icon-unpublish:before {
content: "\e6e5";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "2471358",
"name": "富文本框",
"font_class": "fuwenbenkuang",
"unicode": "e670",
"unicode_decimal": 58992
},
{
"icon_id": "1236927",
"name": "下架",

View File

@ -1,7 +0,0 @@
<template>
<TEditor />
</template>
<script>
</script>

View File

@ -271,22 +271,6 @@
</div>
</el-dialog>
<!--文字组件对话框-->
<el-dialog
v-if="styleDialogVisible && curComponent"
:title="$t('panel.style')"
:visible.sync="styleDialogVisible"
custom-class="de-style-dialog"
>
<PanelTextEditor v-if="curComponent.type==='v-text'" />
<AttrListExtend v-else />
<div style="text-align: center">
<span slot="footer">
<el-button size="mini" @click="closeStyleDialog">{{ $t('commons.confirm') }}</el-button>
</span>
</div>
</el-dialog>
<fullscreen style="height: 100%;background: #f7f8fa;overflow-y: auto" :fullscreen.sync="previewVisible">
<Preview
v-if="previewVisible"
@ -305,7 +289,7 @@
>
<!--矩形样式组件-->
<TextAttr v-show="showAttr" :scroll-left="scrollLeft" :scroll-top="scrollTop" />
<TextAttr v-if="showAttr" :scroll-left="scrollLeft" :scroll-top="scrollTop" />
<!--复用ChartGroup组件 不做显示-->
<ChartGroup
ref="chartGroup"
@ -352,10 +336,8 @@ import { uuid } from 'vue-uuid'
import Toolbar from '@/components/canvas/components/Toolbar'
import { initPanelData, initViewCache } from '@/api/panel/panel'
import Preview from '@/components/canvas/components/Editor/Preview'
import AttrListExtend from '@/components/canvas/components/AttrListExtend'
import elementResizeDetectorMaker from 'element-resize-detector'
import AssistComponent from '@/views/panel/AssistComponent'
import PanelTextEditor from '@/components/canvas/custom-component/PanelTextEditor'
import ChartGroup from '@/views/chart/group/Group'
import { chartCopy } from '@/api/chart/chart'
//
@ -389,9 +371,7 @@ export default {
FilterDialog,
SubjectSetting,
Preview,
AttrListExtend,
AssistComponent,
PanelTextEditor,
TextAttr,
ChartGroup,
ChartEdit