UNPKG

zx-editor

Version:

ZxEditor is a HTML5 rich text editor

414 lines (386 loc) 9.43 kB
/** * Create by zx1984 * 2018/1/23 0023. * https://github.com/zx1984 */ import '../css/index.styl' import './util/polyfill' import dom from './util/dom-core' import util from './util/index' import broadcast from './broadcast/index' import { initMixin } from './init' import {initEvent, checkContentInnerNull, removeContentClass} from './event' import { initEmoji } from './emoji/index' import { initTextStyle } from './text-style/index' import { initLink } from './link' import { initToolbar } from './toolbar' import { toBlobData, filesToBase64, MEDIA_TYPES, createMedia } from './image' /** * Note: * 1. 非特殊说明,带$符号的属性为Element对象 */ class ZxEditor { /** * constructor * @param selector * @param options * @constructor */ constructor (selector, options) { if (this instanceof ZxEditor) { this._init(selector, options) } else { throw new Error('ZxEditor is a constructor and should be called with the `new` keyword') } } /** * 初始化 * @param selector * @param options * @private */ _init (selector, options) { // version this.version = '__VERSION__' // broadcast this.broadcast = broadcast.broadcast // 初始化dom、参数 initMixin(this, selector, options) // 初始化 toolbar initToolbar(this) // 初始化 emojiModal initEmoji(this) // 初始化 textStyleModal initTextStyle(this) // 初始化link initLink(this) // 初始化事件 initEvent(this) this.checkCursorPosition() } /** * 插入dom元素 * @param $el * @param type */ insertElm ($el, type) { if (!$el) { this.emit('error', { msg: `insertElm($el), $el is ${$el}` }) return } // 元素类型 type = type || $el.nodeName.toLowerCase() // console.log($el, type) // 将图片插入至合适位置 this.$cursorElm = dom.insertToRangeElm($el, this.$cursorElm, 'child-node-is-' + type) this.emit('debug', 'insertElm ended') // 重置光标位置 this.cursor.setRange(this.$cursorElm, 0) // 延时执行光标所在元素位置计算 let timer = setTimeout(_ => { this.checkCursorPosition() clearTimeout(timer) timer = null }, 300) } /** * 添加媒体元素 * @param url * @param tag 媒体类型,img、audio、video */ addMedia (url, tag) { this.emit('debug', 'addMedia start', url) // check media type if (!tag) { this.emit('error', { msg: `Unknown media type` }) return } if (MEDIA_TYPES.indexOf(tag) === -1) { this.emit('error', { msg: `Media type "${tag}" is not valid!` }) return } const $media = createMedia(tag, url) this.insertElm($media, tag) } /** * 向文档中添加图片 * @param src */ addImage (src) { this.addMedia(src, 'img') } /** * 添加链接 * @param title * @param url */ addLink (url, title) { this.emit('debug', 'addLink() is start', {url, title}) if (!url) return if (!title) { title = url } let avnode = { tag: 'a', attrs: { href: url, // 'data-url': url, target: '_blank', contenteditable: false }, child: [ title, { tag: 'i', attrs: { class: '__remove' } } ] } // 创建$a元素 const $a = dom.createVdom(avnode) this.insertElm($a) } /** * 添加toolbar button * @param opts */ addFooterButton (opts) { this.emit('debug', 'addFooterButton start') let arr = [] if (util.isObject(opts)) { arr.push(opts) } else if (Array.isArray(opts)) { arr = opts } else { this.emit('error', { msg: 'addFooterButton failure', data: arr }) return } this._addToolbarChild(arr) } _addToolbarChild (arr) { const $dl = dom.query('dl', this.$toolbar) const _this = this let $item, onEvent, vnode arr.forEach(item => { vnode = { tag: 'dd', attrs: { class: `${item.class}`, 'data-name': item.name, 'data-on': item.on }, child: [ { tag: 'i', attrs: { class: item.icon } } ] } $item = dom.createVdom(vnode) _addEvent($item, item) }) // 添加事件 function _addEvent ($item, item) { // 添加事件 dom.addEvent($item, 'click', _ => { _this.emit(item.on, item) }) $dl.appendChild($item) } this.emit('debug', 'addFooterButton ended') } /** * 设置$content底部距离 * @param pos * @param offset 偏移量,使文章内容更容易查看 */ resetContentPostion (pos, offset = 0) { let isFixed = this.options.fixed let styleName = isFixed ? 'bottom' : 'marginBottom' this.$content.style[styleName] = pos + util.int(offset) + 'px' } /** * 获取正文中的base64图片 * @returns {Array} */ getBase64Images () { const arr = [] const $imgs = dom.queryAll('img', this.$content) let $img, base64 for (let i = 0; i < $imgs.length; i++) { $img = $imgs[i] base64 = $img.src if (/^data:.+?;base64,/.test(base64)) { arr.push({ id: $img.id, base64: base64, blob: toBlobData(base64) }) } } return arr } /** * 设置指定id图片src * @param id * @param src * @returns {boolean} */ setImageSrc (id, src) { let $img = dom.query('#' + id, this.$content) if ($img) { $img.src = src $img.removeAttribute('id') return true } return false } /** * 可视区间位置参数 */ _visiblePostion () { // const winW = window.innerWidth const winH = window.innerHeight const opts = this.options let top = util.int(opts.top) // 底部位置 let bottom = 0 // 底部modal容器 // 是否显示 if (this.emojiModal && this.emojiModal.visible) { bottom = this.emojiModal.height } else if (this.textstyleModal && this.textstyleModal.visible) { bottom = this.textstyleModal.height } // 设置的bottom + 底部工具栏高度 else { bottom = util.int(opts.bottom) + (opts.showToolbar ? this.toolbarHeight : 0) } let visiblePosition = { fixed: opts.fixed, // winWidth: winW, winHeight: winH, // startX: 0, // endX: winW, startY: top, endY: winH - bottom - top } this.emit('message', visiblePosition) return visiblePosition } /** * 检查光标元素位置 */ checkCursorPosition () { const vpos = this._visiblePostion() const $el = this.$cursorElm if (!$el) return // 垂直偏移量,使内容滚动位置不要太贴边 const offsetY = 10 const pos = $el.getBoundingClientRect() // 获取滚动容器 let $body = vpos.fixed ? this.$content : dom.query('html') // let bodyScrollHeight = $body.scrollHeight let bodyScrollTop = $body.scrollTop // console.log(bodyScrollTop, document.body.scrollTop) // 不能获取html scrollTop // if (bodyScrollHeight === 0) { // $body = dom.query('body') // console.warn($body.scrollHeight, $body.scrollTop, document.body.scrollTop) // } // console.error(pos.top) if (pos.top < vpos.startY) { $body.scrollTop = bodyScrollTop - (vpos.startY - pos.top) - offsetY } if (pos.bottom > vpos.endY) { $body.scrollTop = bodyScrollTop + vpos.endY + offsetY } } /** * 设置内容 * @param data */ setContent (data) { this.$content.innerHTML = data // 检查内容是否为空 if (!checkContentInnerNull(this.$content)) { removeContentClass(this.$content) } // 重新获取$content 内光标元素 if (this.cursor) { // 初始化完成后 this.$cursorElm = this.cursor.getRange() } } /** * 获取正文内容 * @param isText 只需要文本内容,即不含html标签 * 默认为false,获取html代码 */ getContent (isText = false) { return this.$content[isText ? 'innerText' : 'innerHTML'] } /** * 自动保存 * @param interval 保存间隔时间,单位秒 */ autoSave (interval) { if (typeof interval !== 'number' || interval <= 0) return this.saveTimer = setInterval(_ => { this.save() }, interval * 1000) } /** * 停止自动保存 * @private */ stopAutoSave () { if (this.saveTimer) { clearInterval(this.saveTimer) this.saveTimer = null } } /** * 本地存储 */ save () { this.storage.set('content', this.getContent()) } /** * 移除本地存储的content内容 */ removeSave () { this.storage.remove('content') } } // 扩展属性 ZxEditor.prototype.on = broadcast.on ZxEditor.prototype.off = broadcast.off ZxEditor.prototype.emit = broadcast.emit ZxEditor.prototype.toBlobData = toBlobData ZxEditor.prototype.filesToBase64 = filesToBase64 for (let key in dom) { if (dom.hasOwnProperty(key)) { ZxEditor.prototype[key] = dom[key] } } for (let key in util) { if (util.hasOwnProperty(key)) { ZxEditor.prototype[key] = util[key] } } export { ZxEditor }