quasar-framework
Version:
Build responsive SPA, SSR, PWA, Hybrid Mobile Apps and Electron apps, all simultaneously using the same codebase
361 lines (347 loc) • 11.7 kB
JavaScript
import { getEventKey, stopAndPrevent } from '../../utils/event.js'
import { getToolbar, getFonts, getLinkEditor } from './editor-utils.js'
import { Caret } from './editor-caret.js'
import extend from '../../utils/extend.js'
import FullscreenMixin from '../../mixins/fullscreen.js'
import { isSSR } from '../../plugins/platform.js'
export default {
name: 'QEditor',
mixins: [FullscreenMixin],
props: {
value: {
type: String,
required: true
},
readonly: Boolean,
disable: Boolean,
minHeight: {
type: String,
default: '10rem'
},
maxHeight: String,
height: String,
definitions: Object,
fonts: Object,
toolbar: {
type: Array,
validator: v => v.length === 0 || v.every(group => group.length),
default () {
return [
['left', 'center', 'right', 'justify'],
['bold', 'italic', 'underline', 'strike'],
['undo', 'redo']
]
}
},
toolbarColor: String,
toolbarTextColor: String,
toolbarToggleColor: {
type: String,
default: 'primary'
},
toolbarBg: {
type: String,
default: 'grey-3'
},
toolbarFlat: Boolean,
toolbarOutline: Boolean,
toolbarPush: Boolean,
toolbarRounded: Boolean,
contentStyle: Object,
contentClass: [Object, Array, String]
},
computed: {
editable () {
return !this.readonly && !this.disable
},
hasToolbar () {
return this.toolbar && this.toolbar.length > 0
},
toolbarBackgroundClass () {
if (this.toolbarBg) {
return `bg-${this.toolbarBg}`
}
},
buttonProps () {
return {
outline: this.toolbarOutline,
flat: this.toolbarFlat,
push: this.toolbarPush,
rounded: this.toolbarRounded,
dense: true,
color: this.toolbarColor,
disable: !this.editable
}
},
buttonDef () {
const
e = this.$q.i18n.editor,
i = this.$q.icon.editor
return {
bold: {cmd: 'bold', icon: i.bold, tip: e.bold, key: 66},
italic: {cmd: 'italic', icon: i.italic, tip: e.italic, key: 73},
strike: {cmd: 'strikeThrough', icon: i.strikethrough, tip: e.strikethrough, key: 83},
underline: {cmd: 'underline', icon: i.underline, tip: e.underline, key: 85},
unordered: {cmd: 'insertUnorderedList', icon: i.unorderedList, tip: e.unorderedList},
ordered: {cmd: 'insertOrderedList', icon: i.orderedList, tip: e.orderedList},
subscript: {cmd: 'subscript', icon: i.subscript, tip: e.subscript, htmlTip: 'x<subscript>2</subscript>'},
superscript: {cmd: 'superscript', icon: i.superscript, tip: e.superscript, htmlTip: 'x<superscript>2</superscript>'},
link: {cmd: 'link', icon: i.hyperlink, tip: e.hyperlink, key: 76},
fullscreen: {cmd: 'fullscreen', icon: i.toggleFullscreen, tip: e.toggleFullscreen, key: 70},
quote: {cmd: 'formatBlock', param: 'BLOCKQUOTE', icon: i.quote, tip: e.quote, key: 81},
left: {cmd: 'justifyLeft', icon: i.left, tip: e.left},
center: {cmd: 'justifyCenter', icon: i.center, tip: e.center},
right: {cmd: 'justifyRight', icon: i.right, tip: e.right},
justify: {cmd: 'justifyFull', icon: i.justify, tip: e.justify},
print: {type: 'no-state', cmd: 'print', icon: i.print, tip: e.print, key: 80},
outdent: {type: 'no-state', disable: vm => vm.caret && !vm.caret.can('outdent'), cmd: 'outdent', icon: i.outdent, tip: e.outdent},
indent: {type: 'no-state', disable: vm => vm.caret && !vm.caret.can('indent'), cmd: 'indent', icon: i.indent, tip: e.indent},
removeFormat: {type: 'no-state', cmd: 'removeFormat', icon: i.removeFormat, tip: e.removeFormat},
hr: {type: 'no-state', cmd: 'insertHorizontalRule', icon: i.hr, tip: e.hr},
undo: {type: 'no-state', cmd: 'undo', icon: i.undo, tip: e.undo, key: 90},
redo: {type: 'no-state', cmd: 'redo', icon: i.redo, tip: e.redo, key: 89},
h1: {cmd: 'formatBlock', param: 'H1', icon: i.header, tip: e.header1, htmlTip: `<h1 class="q-ma-none">${e.header1}</h1>`},
h2: {cmd: 'formatBlock', param: 'H2', icon: i.header, tip: e.header2, htmlTip: `<h2 class="q-ma-none">${e.header2}</h2>`},
h3: {cmd: 'formatBlock', param: 'H3', icon: i.header, tip: e.header3, htmlTip: `<h3 class="q-ma-none">${e.header3}</h3>`},
h4: {cmd: 'formatBlock', param: 'H4', icon: i.header, tip: e.header4, htmlTip: `<h4 class="q-ma-none">${e.header4}</h4>`},
h5: {cmd: 'formatBlock', param: 'H5', icon: i.header, tip: e.header5, htmlTip: `<h5 class="q-ma-none">${e.header5}</h5>`},
h6: {cmd: 'formatBlock', param: 'H6', icon: i.header, tip: e.header6, htmlTip: `<h6 class="q-ma-none">${e.header6}</h6>`},
p: {cmd: 'formatBlock', param: 'DIV', icon: i.header, tip: e.paragraph},
code: {cmd: 'formatBlock', param: 'PRE', icon: i.code, tip: `<code>${e.code}</code>`},
'size-1': {cmd: 'fontSize', param: '1', icon: i.size, tip: e.size1, htmlTip: `<font size="1">${e.size1}</font>`},
'size-2': {cmd: 'fontSize', param: '2', icon: i.size, tip: e.size2, htmlTip: `<font size="2">${e.size2}</font>`},
'size-3': {cmd: 'fontSize', param: '3', icon: i.size, tip: e.size3, htmlTip: `<font size="3">${e.size3}</font>`},
'size-4': {cmd: 'fontSize', param: '4', icon: i.size, tip: e.size4, htmlTip: `<font size="4">${e.size4}</font>`},
'size-5': {cmd: 'fontSize', param: '5', icon: i.size, tip: e.size5, htmlTip: `<font size="5">${e.size5}</font>`},
'size-6': {cmd: 'fontSize', param: '6', icon: i.size, tip: e.size6, htmlTip: `<font size="6">${e.size6}</font>`},
'size-7': {cmd: 'fontSize', param: '7', icon: i.size, tip: e.size7, htmlTip: `<font size="7">${e.size7}</font>`}
}
},
buttons () {
const userDef = this.definitions || {}
const def = this.definitions || this.fonts
? extend(
true,
{},
this.buttonDef,
userDef,
getFonts(
this.defaultFont,
this.$q.i18n.editor.defaultFont,
this.$q.icon.editor.font,
this.fonts
)
)
: this.buttonDef
return this.toolbar.map(
group => group.map(token => {
if (token.options) {
return {
type: 'dropdown',
icon: token.icon,
label: token.label,
fixedLabel: token.fixedLabel,
fixedIcon: token.fixedIcon,
highlight: token.highlight,
list: token.list,
options: token.options.map(item => def[item])
}
}
const obj = def[token]
if (obj) {
return obj.type === 'no-state' || (userDef[token] && (
obj.cmd === void 0 || (this.buttonDef[obj.cmd] && this.buttonDef[obj.cmd].type === 'no-state')
))
? obj
: extend(true, { type: 'toggle' }, obj)
}
else {
return {
type: 'slot',
slot: token
}
}
})
)
},
keys () {
const
k = {},
add = btn => {
if (btn.key) {
k[btn.key] = {
cmd: btn.cmd,
param: btn.param
}
}
}
this.buttons.forEach(group => {
group.forEach(token => {
if (token.options) {
token.options.forEach(add)
}
else {
add(token)
}
})
})
return k
},
innerStyle () {
return this.inFullscreen
? this.contentStyle
: [
{
minHeight: this.minHeight,
height: this.height,
maxHeight: this.maxHeight
},
this.contentStyle
]
},
innerClass () {
return [
this.contentClass,
{ col: this.inFullscreen, 'overflow-auto': this.inFullscreen || this.maxHeight }
]
}
},
data () {
return {
editWatcher: true,
editLinkUrl: null
}
},
watch: {
value (v) {
if (this.editWatcher) {
this.$refs.content.innerHTML = v
}
else {
this.editWatcher = true
}
}
},
methods: {
onInput (e) {
if (this.editWatcher) {
const val = this.$refs.content.innerHTML
if (val !== this.value) {
this.editWatcher = false
this.$emit('input', val)
}
}
},
onKeydown (e) {
const key = getEventKey(e)
if (!e.ctrlKey) {
this.refreshToolbar()
this.$q.platform.is.ie && this.$nextTick(this.onInput)
return
}
const target = this.keys[key]
if (target !== void 0) {
const { cmd, param } = target
stopAndPrevent(e)
this.runCmd(cmd, param, false)
this.$q.platform.is.ie && this.$nextTick(this.onInput)
}
},
runCmd (cmd, param, update = true) {
this.focus()
this.caret.apply(cmd, param, () => {
this.focus()
if (update) {
this.refreshToolbar()
}
})
},
refreshToolbar () {
setTimeout(() => {
this.editLinkUrl = null
this.$forceUpdate()
}, 1)
},
focus () {
this.$refs.content.focus()
},
getContentEl () {
return this.$refs.content
}
},
created () {
if (!isSSR) {
document.execCommand('defaultParagraphSeparator', false, 'div')
this.defaultFont = window.getComputedStyle(document.body).fontFamily
}
},
mounted () {
this.$nextTick(() => {
if (this.$refs.content) {
this.caret = new Caret(this.$refs.content, this)
this.$refs.content.innerHTML = this.value
}
this.$nextTick(this.refreshToolbar)
})
},
render (h) {
let toolbars
if (this.hasToolbar) {
const toolbarConfig = {
staticClass: `q-editor-toolbar row no-wrap scroll-x`,
'class': [
{ 'q-editor-toolbar-separator': !this.toolbarOutline && !this.toolbarPush },
this.toolbarBackgroundClass
]
}
toolbars = []
toolbars.push(h('div', extend({key: 'qedt_top'}, toolbarConfig), [
h('div', { staticClass: 'row no-wrap q-editor-toolbar-padding fit items-center' }, getToolbar(h, this))
]))
if (this.editLinkUrl !== null) {
toolbars.push(h('div', extend({key: 'qedt_btm'}, toolbarConfig), [
h('div', { staticClass: 'row no-wrap q-editor-toolbar-padding fit items-center' }, getLinkEditor(h, this))
]))
}
toolbars = h('div', toolbars)
}
return h(
'div',
{
staticClass: 'q-editor',
style: {
height: this.inFullscreen ? '100vh' : null
},
'class': {
disabled: this.disable,
fullscreen: this.inFullscreen,
column: this.inFullscreen
}
},
[
toolbars,
h(
'div',
{
ref: 'content',
staticClass: `q-editor-content`,
style: this.innerStyle,
class: this.innerClass,
attrs: { contenteditable: this.editable },
domProps: isSSR
? { innerHTML: this.value }
: undefined,
on: {
input: this.onInput,
keydown: this.onKeydown,
click: this.refreshToolbar,
blur: () => {
this.caret.save()
}
}
}
)
]
)
}
}