Quill默认工具栏没有提示信息,在playground有Tooltip的实现例子,是基于BootStrap的Tooltip实现的,但我们也可以用较少代码,手写一个Tooltip模块。
CSS实现
工具栏上的图标样式改为相对定位,再给每个图标加上属性tooltip,[tooltip]相对图标定位,再用伪元素编写小箭头和内容,就可以实现提示信息的效果。
index.js
import Quill from 'quill'
import {tooltips} from './tooltip-list.js'
class Tooltip {
constructor (quill) {
this.quill = quill
this.toolbar = quill.getModule('toolbar')
this.buttons = null
this.selectors = null
let toolbarElement = this.toolbar.container
if (toolbarElement) {
this.buttons = toolbarElement.querySelectorAll('button')
this.selectors = toolbarElement.querySelectorAll('.ql-picker')
for (let el of this.buttons) {
this.setTooltip(el)
}
for (let el of this.selectors) {
this.setTooltip(el)
}
}
}
setTooltip (el) {
let format = [].find.call(el.classList, (className) => {
return className.indexOf('ql-') === 0
}).replace('ql-', '')
let tip = ''
if (tooltips[format]) {
let tool = tooltips[format]
if (typeof tool === 'string') {
tip = tool
} else {
let value = el.value || ''
if (value != null && tool[value]) {
tip = tool[value]
}
}
}
Object.assign(el.style, {
'position': 'relative'
})
el.setAttribute('tooltip', tip)
}
}
Quill.register('modules/tooltip', Tooltip)
tooltip.css (参考pure-css-tooltips)
.ql-toolbar [tooltip]{
position:relative;
}
.ql-toolbar [tooltip]::before{
content: "";
position: absolute;
top:-4px;
left:50%;
transform: translateX(-50%);
border-width: 4px 6px 0 6px;
border-style: solid;
border-color: rgba(0,0,0,0.7) transparent transparent transparent;
z-index: 99;
opacity:0;
}
.ql-toolbar [tooltip-position='left']::before{
left:0%;
top:50%;
margin-left:-12px;
transform:translatey(-50%) rotate(-90deg)
}
.ql-toolbar [tooltip-position='top']::before{
left:50%;
}
.ql-toolbar [tooltip-position='bottom']::before{
top:100%;
margin-top:8px;
transform: translateX(-50%) translatey(-100%) rotate(-180deg)
}
.ql-toolbar [tooltip-position='right']::before{
left:100%;
top:50%;
margin-left:1px;
transform:translatey(-50%) rotate(90deg)
}
.ql-toolbar [tooltip]::after {
content: attr(tooltip);
position: absolute;
left:50%;
top:-4px;
transform: translateX(-50%) translateY(-100%);
background: rgba(0,0,0,0.7);
text-align: center;
color: #fff;
padding:4px 2px;
font-size: 12px;
min-width: 70px;
border-radius: 5px;
pointer-events: none;
padding: 4px 4px;
z-index:99;
opacity:0;
}
.ql-toolbar [tooltip-position='left']::after{
left:0%;
top:50%;
margin-left:-8px;
transform: translateX(-100%) translateY(-50%);
}
.ql-toolbar [tooltip-position='top']::after{
left:50%;
}
.ql-toolbar [tooltip-position='bottom']::after{
top:100%;
margin-top:8px;
transform: translateX(-50%) translateY(0%);
}
.ql-toolbar [tooltip-position='right']::after{
left:100%;
top:50%;
margin-left:8px;
transform: translateX(0%) translateY(-50%);
}
.ql-toolbar [tooltip]:hover::after,.ql-toolbar [tooltip]:hover::before {
opacity:1
}
.quill-tooltip::before{
content: "";
position: absolute;
bottom:-4px;
left:50%;
transform: translateX(-50%);
border-width: 4px 6px 0 6px;
border-style: solid;
border-color: rgba(0,0,0,0.7) transparent transparent transparent;
z-index: 99;
}
JS实现
JS实现较为麻烦,需要创建tooltip元素,append到body上,监听mouseenter和mouseleave,显示或隐藏tooltip。但因为是相对body定位,这样的tooltip避免了被页面上其他元素遮挡的问题。
另外,滚动时会出现tooltip跟随鼠标滚动的情况,这时需要获取工具栏所在的可滚动父元素。在其滚动时,隐藏tooltip。
import Quill from 'quill'
import {tooltips} from './tooltip-list.js'
import {getScrollParent} from 'utils/scrollInfo.js'
const tooltipStyles = {
minWidth: '70px',
position: 'absolute',
padding: '4px 8px',
textAlign: 'center',
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#fff',
cursor: 'default',
borderRadius: '4px',
fontSize: '12px',
top: '-9999px',
visibility: 'hidden'
}
class Tooltip {
constructor (quill) {
this.quill = quill
this.toolbar = quill.getModule('toolbar')
this.buttons = null
this.selectors = null
this.tip = null
this.timeout = null
this.mouseenterHandler = null
this.mouseleaveHandler = null
let toolbarElement = this.toolbar.container
if (toolbarElement) {
// 添加处理事件
this.buttons = toolbarElement.querySelectorAll('button')
this.selectors = toolbarElement.querySelectorAll('.ql-picker')
for (let el of this.buttons) {
this.addHandler(el)
}
for (let el of this.selectors) {
this.addHandler(el)
}
// 创建tooltip
this.createTooltip()
// 滚动元素增加handler
this.scrollElm = getScrollParent(toolbarElement)
this.scrollElm.addEventListener('scroll', this.mouseleaveHandler)
}
}
createTooltip () {
this.tip = document.createElement('div')
this.tip.classList.add('quill-tooltip')
Object.assign(this.tip.style, tooltipStyles)
document.body.appendChild(this.tip)
}
addHandler (el) {
this.mouseenterHandler = () => {
this.timeout = setTimeout(() => {
this.showTooltip(el)
}, 100)
}
this.mouseleaveHandler = () => {
if (this.timeout) clearTimeout(this.timeout)
this.hideTooltip()
}
el.addEventListener('mouseenter', this.mouseenterHandler)
el.addEventListener('mouseleave', this.mouseleaveHandler)
}
showTooltip (el) {
// let format = el.className.replace('ql-', '')
let format = [].find.call(el.classList, (className) => {
return className.indexOf('ql-') === 0
}).replace('ql-', '')
if (tooltips[format]) {
let tool = tooltips[format]
if (typeof tool === 'string') {
this.tip.textContent = tool
} else {
let value = el.value || ''
if (value != null && tool[value]) {
this.tip.textContent = tool[value]
}
}
const elRect = el.getBoundingClientRect()
const tipRect = this.tip.getBoundingClientRect()
const body = document.documentElement || document.body
const bodyRect = {
width: body.scrollWidth,
height: body.scrollHeight,
scrollTop: body.scrollTop,
scrollLeft: body.scrollLeft
}
const offset = 3
Object.assign(this.tip.style, {
'top': elRect.top - elRect.height - offset + bodyRect.scrollTop + 'px',
'left': elRect.left - (tipRect.width - elRect.width) / 2 + bodyRect.scrollLeft + 'px',
'visibility': 'visible'
})
}
}
hideTooltip () {
Object.assign(this.tip.style, {
top: '-9999px',
visibility: 'hidden'
})
}
onDestroy () {
console.warn('ondestroy')
this.destroyTooltip()
if (this.buttons) {
for (let el of this.buttons) {
this.removeHandler(el)
}
}
if (this.selectors) {
for (let el of this.selectors) {
this.removeHandler(el)
}
}
if (this.scrollElm) this.scrollElm.removeEventListener('scroll', this.mouseleaveHandler)
}
destroyTooltip () {
if (this.tip.parentNode) this.tip.parentNode.removeChild(this.tip)
}
removeHandler (el) {
el.removeEventListener('mouseenter', this.mouseenterHandler)
el.removeEventListener('mouseleave', this.mouseleaveHandler)
}
}
Quill.register('modules/tooltip', Tooltip)
获取滚动父元素的代码如下(来自ElementUI):
export const getScrollParent = function (element) {
var parent = element.parentNode
if (!parent) {
return element
}
if (parent === document) {
if (document.body.scrollTop !== undefined) {
return document.body
} else {
return document.documentElement
}
}
if (
['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow')) !== -1 ||
['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow-x')) !== -1 ||
['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow-y')) !== -1
) {
return parent
}
return getScrollParent(element.parentNode)
}
export const getStyleComputedProperty = function (element, property) {
var css = window.getComputedStyle(element, null)
return css[property]
}
Tooltip中的文本
文本通过获取图标的className来确定,如果是纯字符串,则直接用tooltips对象查找tooltips[format],如果还有一层,则根据其value进一步查找。
let format = [].find.call(el.classList, (className) => {
return className.indexOf('ql-') === 0
}).replace('ql-', '')
if (tooltips[format]) {
let tool = tooltips[format]
if (typeof tool === 'string') {
this.tip.textContent = tool
} else {
let value = el.value || ''
if (value != null && tool[value]) {
this.tip.textContent = tool[value]
}
}
tooltips如下:
export const tooltips = {
'align': {
'': '对齐',
'center': '居中对齐',
'right': '右对齐',
'justify': '两端对齐'
},
'background': '背景色',
'blockquote': '引用',
'bold': '加粗',
'clean': '清除格式',
'code': '代码',
'code-block': '代码段',
'color': '颜色',
'direction': '方向',
'float': {
'center': '居中',
'full': '全浮动',
'left': '左浮动',
'right': '右浮动'
},
'formula': '公式',
'header': {
'': '标题',
'1': '标题1',
'2': '标题2'
},
'italic': '斜体',
'image': '图片',
'indent': {
'+1': '缩进',
'-1': '取消缩进'
},
'link': '链接',
'list': {
'ordered': '有序列表',
'bullet': '无序列表',
'check': '选择列表'
},
'script': {
'sub': '下标',
'super': '上标'
},
'strike': '删除线',
'underline': '下划线',
'video': '视频',
'undo': '撤销',
'redo': '重做',
'size': '字号',
'font': '字体',
'divider': '分割线',
'formatBrush': '格式刷',
'emoji': '表情'
}