import { extend } from '@opentiny/utils'
import { xss } from '@opentiny/utils'
import { getToolbarTips, defaultOptions } from './options'
import registerTableModule from './table-module'
import registerCustomClipboard from './clipboard'
const registerCustomSize = (Quill, sizeConfig) => {
if (!sizeConfig || !Array.isArray(sizeConfig)) {
return
}
const hasPixelValues = sizeConfig.some((item) => {
return item && typeof item === 'string' && item.includes('px')
})
if (!hasPixelValues) {
return
}
const SizeStyle = Quill.import('attributors/style/size')
const whitelist = sizeConfig.filter((item) => item !== false)
SizeStyle.whitelist = whitelist
Quill.register('formats/size', SizeStyle, true)
}
export const initContent =
({ state, props, nextTick }) =>
() => {
if (state.quill) {
const flag = state.quill.selection.hasFocus()
if (state.content && state.content !== state.innerContent) {
state.innerContent = state.content
state.quill.pasteHTML(xss.filterHtml(state.content))
} else {
if (!state.content) {
state.quill.setText('')
}
}
nextTick(() => {
if (!props.disabled && !props.displayOnly) {
state.quill.enable(true)
}
flag ? state.quill.selection.focus() : state.quill.blur()
})
}
}
export const initQuill =
({ api, emit, props, vm, service, state, Quill, ImageDrop, ImageUpload, FileUpload }) =>
() => {
state.innerOptions = extend(true, {}, defaultOptions, props.globalOptions, props.options)
const toolbarConfig = state.innerOptions.modules?.toolbar?.container || []
const findSizeConfig = (config) => {
if (!config) return null
if (Array.isArray(config)) {
for (let i = 0; i < config.length; i++) {
const result = findSizeConfig(config[i])
if (result) return result
}
} else if (typeof config === 'object' && config !== null) {
if (config.size && Array.isArray(config.size)) {
return config.size
}
for (const key in config) {
const result = findSizeConfig(config[key])
if (result) return result
}
}
return null
}
const sizeConfig = findSizeConfig(toolbarConfig)
if (sizeConfig) {
registerCustomSize(Quill, sizeConfig)
}
if (document.caretRangeFromPoint) {
if (props.imageDrop) {
Quill.register('modules/imageDrop', ImageDrop, true)
state.innerOptions.modules.imageDrop = props.imageDrop
}
if (props.imageUpload) {
Quill.register('modules/imageUpload', ImageUpload, true)
state.innerOptions.modules.imageUpload = props.imageUpload
}
if (props.fileUpload) {
Quill.register('modules/fileUpload', FileUpload, true)
state.innerOptions.modules.fileUpload = extend(
true,
{},
{ httpRequest: service && service.network.request },
props.fileUpload,
{ url: state.fileUploadUrl }
)
}
}
registerTableModule(Quill)
if (state.innerOptions.modules) {
state.innerOptions.modules.tableModule = props.tableModule === true
}
registerCustomClipboard(Quill, props.keepClass)
state.quill = Object.freeze(new Quill(vm.$refs.editor, state.innerOptions))
state.quill.enable(false)
state.quill.on('selection-change', api.selectionChange)
state.quill.on('text-change', api.textChange)
state.quill.root.addEventListener('click', api.handleClick)
if (state.content) {
state.quill.pasteHTML(xss.filterHtml(state.content))
api.textChange()
}
if (!props.disabled && !props.displayOnly) {
state.quill.enable(true)
}
emit('ready', state.quill)
api.setToolbarTips()
api.setTooltipI18n()
state.tooltipI18nHandler = () =>
setTimeout(() => {
api.setTooltipI18n()
api.adjustTooltipPosition()
})
state.tooltipResizeHandler = () => api.adjustTooltipPosition()
vm.$el.addEventListener('click', state.tooltipI18nHandler)
vm.$el.addEventListener('keyup', state.tooltipI18nHandler)
window.addEventListener('resize', state.tooltipResizeHandler)
if (typeof MutationObserver !== 'undefined') {
state.tooltipObserver = new MutationObserver(() => {
requestAnimationFrame(() => api.adjustTooltipPosition())
})
state.tooltipObserver.observe(vm.$el, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'data-mode']
})
}
}
export const handleClick =
({ state, Quill }) =>
(event) => {
const el = event.target
if (!(el instanceof HTMLElement) || el.tagName !== 'IMG') {
return
}
event.stopPropagation()
const imgBlot = Quill.find(el)
const index = state.quill.getIndex(imgBlot)
state.quill.setSelection(index + 1)
}
export const setToolbarTips =
({ t, vm }) =>
() => {
let tip
const item = getToolbarTips(t)
const length = item.length
const richTextEl = vm.$el
for (let i = 0; i < length; i++) {
tip = richTextEl.querySelectorAll('.quill-editor ' + item[i].Choice)
if (tip.length) {
Array.prototype.slice.call(tip).forEach((ite) => {
ite.setAttribute('title', item[i].title)
})
}
}
tip = richTextEl.querySelectorAll('.quill-editor .ql-file')
if (tip.length) {
const iconEl = vm.$refs.iconFile.$el
Array.prototype.slice.call(tip).forEach((ite) => {
ite.innerHTML = getOuterHTML(iconEl)
})
}
}
export const setTooltipI18n =
({ t, vm }) =>
() => {
const richTextEl = vm.$el
const tips = richTextEl.querySelectorAll('.ql-container .ql-tooltip')
const getPlaceholderByMode = (mode) => {
if (mode === 'video') return t('ui.richText.enterVideo')
if (mode === 'formula') return t('ui.richText.enterFormula')
return t('ui.richText.enterLink')
}
if (tips.length) {
Array.prototype.slice.call(tips).forEach((tip) => {
const mode = tip.getAttribute('data-mode') || 'link'
const input = tip.querySelector("input[type='text']")
tip.setAttribute('data-visit-url-text', `${t('ui.richText.visitUrl')}:`)
tip.setAttribute('data-edit-text', t('ui.richText.edit'))
tip.setAttribute('data-remove-text', t('ui.richText.remove'))
tip.setAttribute('data-save-text', t('ui.richText.save'))
tip.setAttribute('data-enter-link-text', `${t('ui.richText.enterLink')}:`)
tip.setAttribute('data-enter-formula-text', `${t('ui.richText.enterFormula')}:`)
tip.setAttribute('data-enter-video-text', `${t('ui.richText.enterVideo')}:`)
if (input) {
input.setAttribute('placeholder', getPlaceholderByMode(mode))
}
})
}
}
export const adjustTooltipPosition =
({ vm }) =>
() => {
const container = vm.$el.querySelector('.ql-container')
const tips = vm.$el.querySelectorAll('.ql-container .ql-tooltip')
const minGap = 8
if (!container) {
return
}
const containerRect = container.getBoundingClientRect()
if (tips.length) {
Array.prototype.slice.call(tips).forEach((tip) => {
if (!tip.classList.contains('ql-editing')) {
return
}
const offsetParent = tip.offsetParent || tip.parentElement
if (!offsetParent) {
return
}
const parentRect = offsetParent.getBoundingClientRect()
const currentLeft = parseFloat(tip.style.left || '0')
const safeCurrentLeft = Number.isFinite(currentLeft) ? currentLeft : 0
const minLeft = containerRect.left - parentRect.left + minGap
const maxLeft = containerRect.right - parentRect.left - tip.offsetWidth - minGap
if (maxLeft <= minLeft) {
tip.style.left = `${minLeft}px`
return
}
const clampedLeft = Math.min(Math.max(safeCurrentLeft, minLeft), maxLeft)
tip.style.left = `${clampedLeft}px`
})
}
}
export const setPlaceholder =
({ state, props }) =>
() => {
if (state.quill) {
state.quill.root.setAttribute('data-placeholder', props.options.placeholder)
}
}
export const getFileUploadUrl =
({ service }) =>
() => {
return service ? service.common.getFileUploadUrl() : Promise.resolve('')
}
export const selectionChange =
({ emit, state }) =>
(range) => {
if (!range) {
emit('blur', state.quill)
} else {
emit('focus', state.quill)
}
}
export const handlePaste =
({ state }) =>
(event) => {
const rangeLength = state.quill.selection.savedRange.length || 0
const newLength = event.clipboardData.getData('text').length || 0
const currentLength = state.quill.getText().length || 0
const totalLength = currentLength - rangeLength + newLength
if (totalLength > state.maxLength) {
event.preventDefault()
state.pasteCanceled = true
} else {
state.pasteCanceled = false
}
}
export const maxLength =
({ props, constants }) =>
() => {
return props.maxLength > 0 ? props.maxLength : constants.MAX_LENGTH
}
export const isDisplayOnly =
({ state, props, parent, nextTick }) =>
() => {
let displayOnly = props.displayOnly || (parent.auiForm || {}).displayOnly
nextTick(() => {
if (state.quill && !props.disabled) {
state.quill.root.setAttribute('contenteditable', !displayOnly)
}
})
return displayOnly
}
export const textChange =
({ emit, vm, state, Modal, t }) =>
(delta, oldDelta) => {
const quill = state.quill
if (state.pasteCanceled) {
state.pasteCanceled = false
quill.setContents(oldDelta)
Modal.message({ message: `${t('ui.richText.maxLength')}${state.maxLength}`, status: 'warning' })
return
}
const text = state.quill.getText()
const currentLength = text.length - 1
const maxLength = state.maxLength
if (currentLength > maxLength) {
quill.setContents(oldDelta)
Modal.message({ message: `${t('ui.richText.maxLength')}${state.maxLength}`, status: 'warning' })
return
}
let html = xss.filterHtml(vm.$refs.editor.children[0].innerHTML)
if (html === '<p><br></p>') html = ''
state.innerContent = html
emit('update:modelValue', html)
emit('change', { html, text, quill })
}
const getOuterHTML = (el) => {
if (!el.outerHTML) {
const container = document.createElement('div')
container.appendChild(el)
return xss.filterHtml(container.innerHTML)
} else {
return xss.filterHtml(el.outerHTML)
}
}
export const mounted =
({ api, props, state, i18n, watch }) =>
() => {
if (props.fileUpload && !props.fileUpload.url) {
api.getFileUploadUrl().then((url) => {
url = xss.filterUrl(url)
state.fileUploadUrl = url
api.initQuill()
})
} else {
api.initQuill()
}
if (i18n) {
watch(() => i18n.locale, () => {
api.setToolbarTips()
api.setTooltipI18n()
api.adjustTooltipPosition()
})
}
}
export const beforeUnmount =
({ api, state, vm }) =>
() => {
state.quill.off('selection-change', api.selectionChange)
state.quill.off('text-change', api.textChange)
state.quill.root.removeEventListener('click', api.handleClick)
state.tooltipI18nHandler && vm.$el.removeEventListener('click', state.tooltipI18nHandler)
state.tooltipI18nHandler && vm.$el.removeEventListener('keyup', state.tooltipI18nHandler)
state.tooltipResizeHandler && window.removeEventListener('resize', state.tooltipResizeHandler)
state.tooltipObserver && state.tooltipObserver.disconnect()
state.tooltipObserver = null
state.quill = null
delete state.quill
}