提交 c7895458 编写于 作者: Q qiang

refactor: editor

上级 018baeab
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.BLOCK,
whitelist: ['left', 'right', 'center', 'justify']
whitelist: ['left', 'right', 'center', 'justify'],
}
const AlignStyle = new Attributor.Style('align', 'text-align', config)
return {
'formats/align': AlignStyle
'formats/align': AlignStyle,
}
}
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const { Scope } = Quill.import('parchment')
const BackgroundStyle = Quill.import('formats/background')
const BackgroundColorStyle = new BackgroundStyle.constructor(
'backgroundColor',
'background-color',
{
scope: Scope.INLINE
scope: Scope.INLINE,
}
)
return {
'formats/backgroundColor': BackgroundColorStyle
'formats/backgroundColor': BackgroundColorStyle,
}
}
import QuillClass from 'quill'
import { hyphenate } from '@vue/shared'
export default function(Quill) {
export default function (Quill: typeof QuillClass) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.BLOCK
scope: Scope.BLOCK,
}
const margin = [
'margin',
'marginTop',
'marginBottom',
'marginLeft',
'marginRight'
'marginRight',
]
const padding = [
'padding',
'paddingTop',
'paddingBottom',
'paddingLeft',
'paddingRight'
'paddingRight',
]
const result = {}
margin.concat(padding).forEach(name => {
const result: Record<string, any> = {}
margin.concat(padding).forEach((name) => {
result[`formats/${name}`] = new Attributor.Style(
name,
hyphenate(name),
......
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.BLOCK,
whitelist: ['rtl']
whitelist: ['rtl'],
}
const DirectionStyle = new Attributor.Style('direction', 'direction', config)
return {
'formats/direction': DirectionStyle
'formats/direction': DirectionStyle,
}
}
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const BlockEmbed = Quill.import('blots/block/embed')
class Divider extends BlockEmbed {}
Divider.blotName = 'divider'
Divider.tagName = 'HR'
return {
'formats/divider': Divider
'formats/divider': Divider,
}
}
import QuillClass from 'quill'
import { hyphenate } from '@vue/shared'
export default function(Quill) {
export default function (Quill: typeof QuillClass) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.INLINE
scope: Scope.INLINE,
}
const font = [
'font',
......@@ -11,10 +12,10 @@ export default function(Quill) {
'fontStyle',
'fontVariant',
'fontWeight',
'fontFamily'
'fontFamily',
]
const result = {}
font.forEach(name => {
const result: Record<string, any> = {}
font.forEach((name) => {
result[`formats/${name}`] = new Attributor.Style(
name,
hyphenate(name),
......
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const Image = Quill.import('formats/image')
const ATTRIBUTES = [
'alt',
......@@ -6,19 +8,23 @@ export default function(Quill) {
'width',
'data-custom',
'class',
'data-local'
'data-local',
]
Image.sanitize = url => url
Image.formats = function formats(domNode) {
return ATTRIBUTES.reduce(function(formats, attribute) {
Image.sanitize = (url: string) => url
Image.formats = function formats(domNode: Element) {
return ATTRIBUTES.reduce(function (
formats: Record<string, any>,
attribute
) {
if (domNode.hasAttribute(attribute)) {
formats[attribute] = domNode.getAttribute(attribute)
}
return formats
}, {})
},
{})
}
const format = Image.prototype.format
Image.prototype.format = function(name, value) {
Image.prototype.format = function (name: string, value: string) {
if (ATTRIBUTES.indexOf(name) > -1) {
if (value) {
this.domNode.setAttribute(name, value)
......
import QuillClass from 'quill'
import divider from './divider'
import ins from './ins'
import align from './align'
......@@ -9,7 +11,7 @@ import font from './font'
import text from './text'
import image from './image'
export function register(Quill) {
export function register(Quill: typeof QuillClass) {
const formats = {
divider,
ins,
......@@ -20,9 +22,11 @@ export function register(Quill) {
box,
font,
text,
image
image,
}
const options = {}
Object.values(formats).forEach(value => Object.assign(options, value(Quill)))
Object.values(formats).forEach((value) =>
Object.assign(options, value(Quill))
)
Quill.register(options, true)
}
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const Inline = Quill.import('blots/inline')
class Ins extends Inline {}
Ins.blotName = 'ins'
Ins.tagName = 'INS'
return {
'formats/ins': Ins
'formats/ins': Ins,
}
}
export default function(Quill) {
import QuillClass from 'quill'
export default function (Quill: typeof QuillClass) {
const Parchment = Quill.import('parchment')
const Container = Quill.import('blots/container')
const ListItem = Quill.import('formats/list/item')
class List extends Container {
static create(value) {
static create(value: string) {
const tagName = value === 'ordered' ? 'OL' : 'UL'
const node = super.create(tagName)
if (value === 'checked' || value === 'unchecked') {
......@@ -13,7 +15,7 @@ export default function(Quill) {
return node
}
static formats(domNode) {
static formats(domNode: HTMLElement) {
if (domNode.tagName === 'OL') return 'ordered'
if (domNode.tagName === 'UL') {
if (domNode.hasAttribute('data-checked')) {
......@@ -27,10 +29,10 @@ export default function(Quill) {
return undefined
}
constructor(domNode) {
constructor(domNode: HTMLElement) {
super(domNode)
const listEventHandler = e => {
if (e.target.parentNode !== domNode) return
const listEventHandler = (e: Event) => {
if ((e.target as HTMLElement).parentNode !== domNode) return
const format = this.statics.formats(domNode)
const blot = Parchment.find(e.target)
if (format === 'checked') {
......@@ -43,7 +45,7 @@ export default function(Quill) {
domNode.addEventListener('click', listEventHandler)
}
format(name, value) {
format(name: string, value: string) {
if (this.children.length > 0) {
this.children.tail.format(name, value)
}
......@@ -54,7 +56,7 @@ export default function(Quill) {
return { [this.statics.blotName]: this.statics.formats(this.domNode) }
}
insertBefore(blot, ref) {
insertBefore(blot: any, ref: any) {
if (blot instanceof ListItem) {
super.insertBefore(blot, ref)
} else {
......@@ -64,7 +66,7 @@ export default function(Quill) {
}
}
optimize(context) {
optimize(context: any) {
super.optimize(context)
const next = this.next
if (
......@@ -80,7 +82,7 @@ export default function(Quill) {
}
}
replace(target) {
replace(target: any) {
if (target.statics.blotName !== this.statics.blotName) {
const item = Parchment.create(this.statics.defaultChild)
target.moveChildren(item)
......@@ -96,6 +98,6 @@ export default function(Quill) {
List.allowedChildren = [ListItem]
return {
'formats/list': List
'formats/list': List,
}
}
import QuillClass from 'quill'
import { hyphenate } from '@vue/shared'
export default function(Quill) {
export default function (Quill: typeof QuillClass) {
const { Scope, Attributor } = Quill.import('parchment')
const text = [
{
name: 'lineHeight',
scope: Scope.BLOCK
scope: Scope.BLOCK,
},
{
name: 'letterSpacing',
scope: Scope.INLINE
scope: Scope.INLINE,
},
{
name: 'textDecoration',
scope: Scope.INLINE
scope: Scope.INLINE,
},
{
name: 'textIndent',
scope: Scope.BLOCK
}
scope: Scope.BLOCK,
},
]
const result = {}
const result: Record<string, any> = {}
text.forEach(({ name, scope }) => {
result[`formats/${name}`] = new Attributor.Style(name, hyphenate(name), {
scope
scope,
})
})
......
import { defineComponent, onMounted, Ref, ref, watch } from 'vue'
import QuillClass, {
QuillOptionsStatic,
EventEmitter,
RangeStatic,
StringMap,
} from 'quill'
import { useSubscribe } from '@dcloudio/uni-components'
import { getRealPath } from '@dcloudio/uni-platform'
import { CustomEventTrigger, useCustomEvent } from '../../helpers/useEvent'
import { useKeyboard } from '../../helpers/useKeyboard'
import HTMLParser from '../../helpers/html-parser'
import * as formats from './formats'
import loadScript from './load-script'
type EDITOR_CHANGE = 'editor-change'
type SCROLL_BEFORE_UPDATE = 'scroll-before-update'
type SCROLL_OPTIMIZE = 'scroll-optimize'
type SCROLL_UPDATE = 'scroll-update'
type SELECTION_CHANGE = 'selection-change'
type TEXT_CHANGE = 'text-change'
interface QuillSelection {
getRange: () => RangeStatic[]
savedRange: RangeStatic
}
interface QuillHistory {
undo: () => void
redo: () => void
}
interface QuillExt extends QuillClass {
selection: QuillSelection
on: (
eventName:
| EDITOR_CHANGE
| SCROLL_BEFORE_UPDATE
| SCROLL_OPTIMIZE
| SCROLL_UPDATE
| SELECTION_CHANGE
| TEXT_CHANGE,
handler: Function
) => EventEmitter
scrollIntoView: () => void
history: QuillHistory
}
interface QuillOptionsStaticExt extends QuillOptionsStatic {
toolbar?: boolean
}
interface WindowExt extends Window {
Quill?: typeof QuillClass
ImageResize?: any
}
function useQuill(
props: {
readOnly?: any
placeholder?: any
showImgSize?: any
showImgToolbar?: any
showImgResize?: any
},
rootRef: Ref<HTMLElement | null>,
trigger: CustomEventTrigger
) {
type ResizeModuleName = 'DisplaySize' | 'Toolbar' | 'Resize'
let quillReady: boolean
let skipMatcher: boolean
let quill: QuillExt
watch(
() => props.readOnly,
(value) => {
if (quillReady) {
quill.enable(!value)
if (!value) {
quill.blur()
}
}
}
)
watch(
() => props.placeholder,
(value) => {
if (quillReady) {
quill.root.setAttribute('data-placeholder', value)
}
}
)
function html2delta(html: string) {
const tags = [
'span',
'strong',
'b',
'ins',
'em',
'i',
'u',
'a',
'del',
's',
'sub',
'sup',
'img',
'div',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'ol',
'ul',
'li',
'br',
]
let content = ''
let disable: boolean
HTMLParser(html, {
start: function (tag: string, attrs: any[], unary: boolean) {
if (!tags.includes(tag)) {
disable = !unary
return
}
disable = false
const arrts = attrs
.map(({ name, value }) => `${name}="${value}"`)
.join(' ')
const start = `<${tag} ${arrts} ${unary ? '/' : ''}>`
content += start
},
end: function (tag: string) {
if (!disable) {
content += `</${tag}>`
}
},
chars: function (text: string) {
if (!disable) {
content += text
}
},
})
skipMatcher = true
const delta = quill.clipboard.convert(content)
skipMatcher = false
return delta
}
function getContents() {
const html = quill.root.innerHTML
const text = quill.getText()
const delta = quill.getContents()
return {
html,
text,
delta,
}
}
let oldStatus: StringMap = {}
function updateStatus(range?: RangeStatic) {
const status = range ? quill.getFormat(range) : {}
const keys = Object.keys(status)
if (
keys.length !== Object.keys(oldStatus).length ||
keys.find((key) => status[key] !== oldStatus[key])
) {
oldStatus = status
trigger('statuschange', {} as Event, status)
}
}
function initQuill(imageResizeModules: ResizeModuleName[]) {
const Quill = (window as WindowExt).Quill as typeof QuillClass
formats.register(Quill)
const options: QuillOptionsStaticExt = {
toolbar: false,
readOnly: props.readOnly,
placeholder: props.placeholder,
}
if (imageResizeModules.length) {
Quill.register(
'modules/ImageResize',
(window as WindowExt).ImageResize.default
)
options.modules = {
ImageResize: {
modules: imageResizeModules,
},
}
}
const rootEl = rootRef.value as HTMLElement
quill = new Quill(rootEl, options) as QuillExt
const $el = quill.root
const events = ['focus', 'blur', 'input']
events.forEach((name) => {
$el.addEventListener(name, ($event) => {
if (name === 'input') {
$event.stopPropagation()
} else {
trigger(name, $event, getContents())
}
})
})
quill.on('text-change', () => {
trigger('input', {} as Event, getContents())
})
quill.on('selection-change', updateStatus)
quill.on('scroll-optimize', () => {
const range = quill.selection.getRange()[0]
updateStatus(range)
})
quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
if (skipMatcher) {
return delta
}
if (delta.ops) {
delta.ops = delta.ops
.filter(({ insert }) => typeof insert === 'string')
.map(({ insert }) => ({ insert }))
}
return delta
})
quillReady = true
trigger('ready', {} as Event, {})
}
onMounted(() => {
const imageResizeModules: ResizeModuleName[] = []
if (props.showImgSize) {
imageResizeModules.push('DisplaySize')
}
if (props.showImgToolbar) {
imageResizeModules.push('Toolbar')
}
if (props.showImgResize) {
imageResizeModules.push('Resize')
}
const quillSrc =
__PLATFORM__ === 'app'
? './__uniappquill.js'
: 'https://unpkg.com/quill@1.3.7/dist/quill.min.js'
loadScript((window as WindowExt).Quill, quillSrc, () => {
if (imageResizeModules.length) {
const imageResizeSrc =
__PLATFORM__ === 'app'
? './__uniappquillimageresize.js'
: 'https://unpkg.com/quill-image-resize-mp@3.0.1/image-resize.min.js'
loadScript((window as WindowExt).ImageResize, imageResizeSrc, () => {
initQuill(imageResizeModules)
})
} else {
initQuill(imageResizeModules)
}
})
})
useSubscribe((type: string, data: any) => {
const { options, callbackId } = data
let res
let range: RangeStatic | undefined
let errMsg
if (quillReady) {
const Quill = (window as WindowExt).Quill as typeof QuillClass
switch (type) {
case 'format':
{
let { name = '', value = false } = options
range = quill.getSelection(true)
let format = quill.getFormat(range)[name] || false
if (
['bold', 'italic', 'underline', 'strike', 'ins'].includes(name)
) {
value = !format
} else if (name === 'direction') {
value = value === 'rtl' && format ? false : value
const align = quill.getFormat(range).align
if (value === 'rtl' && !align) {
quill.format('align', 'right', 'user')
} else if (!value && align === 'right') {
quill.format('align', false, 'user')
}
} else if (name === 'indent') {
const rtl = quill.getFormat(range).direction === 'rtl'
value = value === '+1'
if (rtl) {
value = !value
}
value = value ? '+1' : '-1'
} else {
if (name === 'list') {
value = value === 'check' ? 'unchecked' : value
format = format === 'checked' ? 'unchecked' : format
}
value =
(format && format !== (value || false)) || (!format && value)
? value
: !format
}
quill.format(name, value, 'user')
}
break
case 'insertDivider':
range = quill.getSelection(true)
quill.insertText(range.index, '\n', 'user')
quill.insertEmbed(range.index + 1, 'divider', true, 'user')
quill.setSelection(range.index + 2, 0, 'silent')
break
case 'insertImage':
{
range = quill.getSelection(true)
const {
src = '',
alt = '',
width = '',
height = '',
extClass = '',
data = {},
} = options
const path = getRealPath(src)
quill.insertEmbed(range.index, 'image', path, 'user')
const local = /^(file|blob):/.test(path) ? path : false
quill.formatText(range.index, 1, 'data-local', local)
quill.formatText(range.index, 1, 'alt', alt)
quill.formatText(range.index, 1, 'width', width)
quill.formatText(range.index, 1, 'height', height)
quill.formatText(range.index, 1, 'class', extClass)
quill.formatText(
range.index,
1,
'data-custom',
Object.keys(data)
.map((key) => `${key}=${data[key]}`)
.join('&')
)
quill.setSelection(range.index + 1, 0, 'silent')
}
break
case 'insertText':
{
range = quill.getSelection(true)
const { text = '' } = options
quill.insertText(range.index, text, 'user')
quill.setSelection(range.index + text.length, 0, 'silent')
}
break
case 'setContents':
{
const { delta, html } = options
if (typeof delta === 'object') {
quill.setContents(delta, 'silent')
} else if (typeof html === 'string') {
quill.setContents(html2delta(html), 'silent')
} else {
errMsg = 'contents is missing'
}
}
break
case 'getContents':
res = getContents()
break
case 'clear':
quill.setText('')
break
case 'removeFormat':
{
range = quill.getSelection(true)
const parchment = Quill.import('parchment')
if (range.length) {
quill.removeFormat(range.index, range.length, 'user')
} else {
Object.keys(quill.getFormat(range)).forEach((key) => {
if (parchment.query(key, parchment.Scope.INLINE)) {
quill.format(key, false)
}
})
}
}
break
case 'undo':
quill.history.undo()
break
case 'redo':
quill.history.redo()
break
case 'blur':
quill.blur()
break
case 'getSelectionText':
range = quill.selection.savedRange
res = { text: '' }
if (range && range.length !== 0) {
res.text = quill.getText(range.index, range.length)
}
break
case 'scrollIntoView':
quill.scrollIntoView()
break
default:
break
}
updateStatus(range)
} else {
errMsg = 'not ready'
}
if (callbackId) {
UniViewJSBridge.publishHandler('onEditorMethodCallback', {
callbackId,
data: Object.assign({}, res, {
errMsg: `${type}:${errMsg ? 'fail ' + errMsg : 'ok'}`,
}),
})
}
})
}
const props = {
id: {
type: String,
default: '',
},
readOnly: {
type: [Boolean, String],
default: false,
},
placeholder: {
type: String,
default: '',
},
showImgSize: {
type: [Boolean, String],
default: false,
},
showImgToolbar: {
type: [Boolean, String],
default: false,
},
showImgResize: {
type: [Boolean, String],
default: false,
},
disabled: {
type: [Boolean, String],
default: false,
},
cursorSpacing: {
type: [Number, String],
default: 0,
},
showConfirmBar: {
type: [Boolean, String],
default: 'auto',
},
adjustPosition: {
type: [Boolean, String],
default: true,
},
autoBlur: {
type: [Boolean, String],
default: false,
},
}
export default defineComponent({
name: 'Editor',
props,
setup(props, { emit }) {
const rootRef: Ref<HTMLElement | null> = ref(null)
const trigger = useCustomEvent(rootRef, emit)
useQuill(props, rootRef, trigger)
useKeyboard(props, rootRef, trigger)
return () => {
return <uni-editor ref={rootRef} id={props.id} class="ql-container" />
}
},
})
<template>
<uni-editor
:id="id"
class="ql-container"
v-bind="$attrs"
/>
</template>
<script>
import {
subscriber,
emitter,
keyboard
} from '../../mixins'
import HTMLParser from '../../helpers/html-parser'
import * as formats from './formats'
export default {
name: 'Editor',
mixins: [subscriber, emitter, keyboard],
props: {
id: {
type: String,
default: ''
},
readOnly: {
type: [Boolean, String],
default: false
},
placeholder: {
type: String,
default: ''
},
showImgSize: {
type: [Boolean, String],
default: false
},
showImgToolbar: {
type: [Boolean, String],
default: false
},
showImgResize: {
type: [Boolean, String],
default: false
}
},
data () {
return {
quillReady: false
}
},
computed: {
},
watch: {
readOnly (value) {
if (this.quillReady) {
const quill = this.quill
quill.enable(!value)
if (!value) {
quill.blur()
}
}
},
placeholder (value) {
if (this.quillReady) {
this.quill.root.setAttribute('data-placeholder', value)
}
}
},
mounted () {
const imageResizeModules = []
if (this.showImgSize) {
imageResizeModules.push('DisplaySize')
}
if (this.showImgToolbar) {
imageResizeModules.push('Toolbar')
}
if (this.showImgResize) {
imageResizeModules.push('Resize')
}
this.loadQuill(() => {
if (imageResizeModules.length) {
this.loadImageResizeModule(() => {
this.initQuill(imageResizeModules)
})
} else {
this.initQuill(imageResizeModules)
}
})
},
methods: {
_handleSubscribe ({
type,
data
}) {
const { options, callbackId } = data
const quill = this.quill
const Quill = window.Quill
let res
let range
let errMsg
if (this.quillReady) {
switch (type) {
case 'format':
{
let { name = '', value = false } = options
range = quill.getSelection(true)
let format = quill.getFormat(range)[name] || false
if (['bold', 'italic', 'underline', 'strike', 'ins'].includes(name)) {
value = !format
} else if (name === 'direction') {
value = value === 'rtl' && format ? false : value
const align = quill.getFormat(range).align
if (value === 'rtl' && !align) {
quill.format('align', 'right', Quill.sources.USER)
} else if (!value && align === 'right') {
quill.format('align', false, Quill.sources.USER)
}
} else if (name === 'indent') {
const rtl = quill.getFormat(range).direction === 'rtl'
value = value === '+1'
if (rtl) {
value = !value
}
value = value ? '+1' : '-1'
} else {
if (name === 'list') {
value = value === 'check' ? 'unchecked' : value
format = format === 'checked' ? 'unchecked' : format
}
value = ((format && format !== (value || false)) || (!format && value)) ? value : !format
}
quill.format(name, value, Quill.sources.USER)
}
break
case 'insertDivider':
range = quill.getSelection(true)
quill.insertText(range.index, '\n', Quill.sources.USER)
quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER)
quill.setSelection(range.index + 2, Quill.sources.SILENT)
break
case 'insertImage':
{
range = quill.getSelection(true)
const { src = '', alt = '', width = '', height = '', extClass = '', data = {} } = options
const path = this.$getRealPath(src)
quill.insertEmbed(range.index, 'image', path, Quill.sources.USER)
const local = /^(file|blob):/.test(path) ? path : false
quill.formatText(range.index, 1, 'data-local', local)
quill.formatText(range.index, 1, 'alt', alt)
quill.formatText(range.index, 1, 'width', width)
quill.formatText(range.index, 1, 'height', height)
quill.formatText(range.index, 1, 'class', extClass)
quill.formatText(range.index, 1, 'data-custom', Object.keys(data).map(key => `${key}=${data[key]}`).join('&'))
quill.setSelection(range.index + 1, Quill.sources.SILENT)
}
break
case 'insertText':
{
range = quill.getSelection(true)
const { text = '' } = options
quill.insertText(range.index, text, Quill.sources.USER)
quill.setSelection(range.index + text.length, 0, Quill.sources.SILENT)
}
break
case 'setContents':
{
const { delta, html } = options
if (typeof delta === 'object') {
quill.setContents(delta, Quill.sources.SILENT)
} else if (typeof html === 'string') {
quill.setContents(this.html2delta(html), Quill.sources.SILENT)
} else {
errMsg = 'contents is missing'
}
}
break
case 'getContents':
res = this.getContents()
break
case 'clear':
quill.setContents([])
break
case 'removeFormat':
{
range = quill.getSelection(true)
const parchment = Quill.import('parchment')
if (range.length) {
quill.removeFormat(range, Quill.sources.USER)
} else {
Object.keys(quill.getFormat(range)).forEach(key => {
if (parchment.query(key, parchment.Scope.INLINE)) {
quill.format(key, false)
}
})
}
}
break
case 'undo':
quill.history.undo()
break
case 'redo':
quill.history.redo()
break
default:
break
}
this.updateStatus(range)
} else {
errMsg = 'not ready'
}
if (callbackId) {
UniViewJSBridge.publishHandler('onEditorMethodCallback', {
callbackId,
data: Object.assign({}, res, {
errMsg: `${type}:${errMsg ? 'fail ' + errMsg : 'ok'}`
})
}, this.$page.id)
}
},
loadQuill (callback) {
if (typeof window.Quill === 'function') {
if (typeof callback === 'function') {
callback()
}
return
}
const script = document.createElement('script')
script.src = window.plus ? './__uniappquill.js' : 'https://unpkg.com/quill@1.3.7/dist/quill.min.js'
document.body.appendChild(script)
script.onload = callback
},
loadImageResizeModule (callback) {
if (typeof window.ImageResize === 'function') {
if (typeof callback === 'function') {
callback()
}
return
}
const script = document.createElement('script')
script.src = window.plus ? './__uniappquillimageresize.js' : 'https://unpkg.com/quill-image-resize-mp@3.0.1/image-resize.min.js'
document.body.appendChild(script)
script.onload = callback
},
initQuill (imageResizeModules) {
const Quill = window.Quill
formats.register(Quill)
const options = {
toolbar: false,
readOnly: this.readOnly,
placeholder: this.placeholder,
modules: {}
}
if (imageResizeModules.length) {
Quill.register('modules/ImageResize', window.ImageResize.default)
options.modules.ImageResize = {
modules: imageResizeModules
}
}
const quill = this.quill = new Quill(this.$el, options)
const $el = quill.root
const events = ['focus', 'blur', 'input']
events.forEach(name => {
$el.addEventListener(name, ($event) => {
if (name === 'input') {
$event.stopPropagation()
} else {
this.$trigger(name, $event, this.getContents())
}
})
})
quill.on(Quill.events.TEXT_CHANGE, () => {
this.$trigger('input', {}, this.getContents())
})
quill.on(Quill.events.SELECTION_CHANGE, this.updateStatus.bind(this))
quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
const range = quill.selection.getRange()[0]
this.updateStatus(range)
})
quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
if (this.skipMatcher) {
return delta
}
delta.ops = delta.ops.filter(({ insert }) => typeof insert === 'string').map(({ insert }) => ({ insert }))
return delta
})
this.initKeyboard($el)
this.quillReady = true
this.$trigger('ready', event, {})
},
getContents () {
const quill = this.quill
const html = quill.root.innerHTML
const text = quill.getText()
const delta = quill.getContents()
return {
html,
text,
delta
}
},
html2delta (html) {
const tags = ['span', 'strong', 'b', 'ins', 'em', 'i', 'u', 'a', 'del', 's', 'sub', 'sup', 'img', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'ol', 'ul', 'li', 'br']
let content = ''
let disable
HTMLParser(html, {
start: function (tag, attrs, unary) {
if (!tags.includes(tag)) {
disable = !unary
return
}
disable = false
const arrts = attrs.map(({ name, value }) => `${name}="${value}"`).join(' ')
const start = `<${tag} ${arrts} ${unary ? '/' : ''}>`
content += start
},
end: function (tag) {
if (!disable) {
content += `</${tag}>`
}
},
chars: function (text) {
if (!disable) {
content += text
}
}
})
this.skipMatcher = true
const delta = this.quill.clipboard.convert(content)
this.skipMatcher = false
return delta
},
updateStatus (range) {
const status = range ? this.quill.getFormat(range) : {}
const keys = Object.keys(status)
if (keys.length !== Object.keys(this.__status || {}).length || keys.find(key => status[key] !== this.__status[key])) {
this.__status = status
this.$trigger('statuschange', {}, status)
}
}
}
}
</script>
\ No newline at end of file
const scripts: Record<string, Function[]> = {}
interface WindowExt extends Window {
[key: string]: any
}
export default function loadScript(
globalName: any,
src: string,
callback: () => void
) {
const globalObject =
typeof globalName === 'string'
? (window as WindowExt)[globalName]
: globalName
if (globalObject) {
callback()
return
}
let callbacks = scripts[src]
if (!callbacks) {
callbacks = scripts[src] = []
const script = document.createElement('script')
script.src = src
document.body.appendChild(script)
script.onload = function () {
callbacks.forEach((callback) => callback())
delete scripts[src]
}
}
callbacks.push(callback)
}
......@@ -3,7 +3,7 @@ import Button from './button/index'
import Canvas from './canvas/index.vue'
import Checkbox from './checkbox/index.vue'
import CheckboxGroup from './checkbox-group/index.vue'
import Editor from './editor/index.vue'
import Editor from './editor/index'
import Form from './form/index'
import Icon from './icon/index'
import Image from './image/index'
......
import { Ref, onMounted } from 'vue'
import { CustomEventTrigger } from './useEvent'
import { plusReady } from '@dcloudio/uni-shared'
let resetTimer: number
let isAndroid: boolean
let osVersion: string
let keyboardHeight: number
let keyboardChangeCallback: Function | null
interface KeyboardChangeEvent extends Event {
height: number
}
if (__PLATFORM__ === 'app') {
plusReady(() => {
isAndroid = plus.os.name === 'Android'
osVersion = plus.os.version || ''
})
document.addEventListener(
'keyboardchange',
function (event: Event) {
keyboardHeight = (event as KeyboardChangeEvent).height
keyboardChangeCallback && keyboardChangeCallback()
},
false
)
}
/**
* 保证iOS点击输入框外隐藏键盘
*/
function iosHideKeyboard() {}
function setSoftinputTemporary(
props: { cursorSpacing: number | string; adjustPosition: any },
el: HTMLElement,
reset?: boolean
) {
plusReady(() => {
const MODE_ADJUSTRESIZE = 'adjustResize'
const MODE_ADJUSTPAN = 'adjustPan'
const MODE_NOTHING = 'nothing'
const currentWebview = plus.webview.currentWebview()
const style = currentWebview.getStyle() || {}
const options = {
mode:
reset || style.softinputMode === MODE_ADJUSTRESIZE
? MODE_ADJUSTRESIZE
: props.adjustPosition
? MODE_ADJUSTPAN
: MODE_NOTHING,
position: {
top: 0,
height: 0,
},
}
if (options.mode === MODE_ADJUSTPAN) {
const rect = el.getBoundingClientRect()
options.position.top = rect.top
options.position.height = rect.height + (Number(props.cursorSpacing) || 0)
}
currentWebview.setSoftinputTemporary(options)
})
}
function setSoftinputNavBar(
props: { showConfirmBar: any },
state: KeyboardState
) {
if (props.showConfirmBar === 'auto') {
delete state.softinputNavBar
return
}
plusReady(() => {
const currentWebview = plus.webview.currentWebview()
const { softinputNavBar } = currentWebview.getStyle() || {}
const showConfirmBar = softinputNavBar !== 'none'
if (showConfirmBar !== props.showConfirmBar) {
state.softinputNavBar = softinputNavBar || 'auto'
currentWebview.setStyle({
softinputNavBar: props.showConfirmBar ? 'auto' : 'none',
})
} else {
delete state.softinputNavBar
}
})
}
function resetSoftinputNavBar(state: KeyboardState) {
const softinputNavBar = state.softinputNavBar
if (softinputNavBar) {
plusReady(() => {
const currentWebview = plus.webview.currentWebview()
currentWebview.setStyle({
softinputNavBar,
})
})
}
}
interface KeyboardState {
softinputNavBar?: any
}
export function useKeyboard(
props: {
disabled: any
cursorSpacing: number | string
autoBlur: any
adjustPosition: any
showConfirmBar: any
},
elRef: Ref<HTMLElement | null>,
trigger: CustomEventTrigger
) {
const state = {}
function initKeyboard(el: HTMLElement) {
let focus: boolean
const keyboardChange = () => {
trigger('keyboardheightchange', {} as Event, {
height: keyboardHeight,
duration: 0.25,
})
// 安卓切换不同键盘类型时会导致键盘收回,需重新设置
if (focus && keyboardHeight === 0) {
setSoftinputTemporary(props, el)
}
// 安卓/iOS13收起键盘时主动失去焦点
if (
props.autoBlur &&
focus &&
keyboardHeight === 0 &&
(isAndroid || parseInt(osVersion) >= 13)
) {
;(document.activeElement as HTMLElement).blur()
}
}
el.addEventListener('focus', () => {
focus = true
clearTimeout(resetTimer)
document.addEventListener('click', iosHideKeyboard, false)
if (__PLATFORM__ === 'app') {
keyboardChangeCallback = keyboardChange
if (keyboardHeight) {
trigger('keyboardheightchange', {} as Event, {
height: keyboardHeight,
duration: 0,
})
}
setSoftinputNavBar(props, state)
setSoftinputTemporary(props, el)
}
})
if (__PLATFORM__ === 'app') {
// 安卓单独隐藏键盘后点击输入框不会触发 focus 事件
el.addEventListener('click', () => {
if (!props.disabled && focus && keyboardHeight === 0) {
setSoftinputTemporary(props, el)
}
})
if (!isAndroid && parseInt(osVersion) < 12) {
// iOS12 以下系统 focus 事件设置较迟,改在 touchstart 设置
el.addEventListener('touchstart', () => {
if (!props.disabled && !focus) {
setSoftinputTemporary(props, el)
}
})
}
}
const onKeyboardHide = () => {
document.removeEventListener('click', iosHideKeyboard, false)
if (__PLATFORM__ === 'app') {
keyboardChangeCallback = null
if (keyboardHeight) {
trigger('keyboardheightchange', {} as Event, {
height: 0,
duration: 0,
})
}
resetSoftinputNavBar(state)
if (isAndroid) {
// 还原安卓软键盘配置,避免影响 web-view 组件
resetTimer = setTimeout(() => {
setSoftinputTemporary(props, el, true)
}, 300)
}
}
// 修复ios端显示与点击位置错位的Bug by:wyq
if (String(navigator.vendor).indexOf('Apple') === 0) {
document.documentElement.scrollTo(
document.documentElement.scrollLeft,
document.documentElement.scrollTop
)
}
}
el.addEventListener('blur', () => {
focus = false
onKeyboardHide()
})
}
onMounted(() => {
const el = elRef.value as HTMLElement
initKeyboard(el)
})
}
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册