UNPKG

zx-editor

Version:

ZxEditor is a HTML5 rich text editor

487 lines (446 loc) 12.6 kB
/** * Created by Capricorncd. * Date: 2019/04/12 11:12 * Copyright © 2017-present, https://github.com/capricorncd */ /** * *********************************************** * Notes: * 1. The property or variables prefixed $ are ZxQuery instance, or related to them * *********************************************** */ import $ from './dom-class/index' import CursorClass from './cursor-class/index' import ExpansionPanel from './expansion-panel/index' import util from './util/index' import { initDom } from './init/init-dom' import { initStyle } from './init/init-style' import { handleEvents } from './events/index' import { extendPrototypes } from './init/extend-prototype' import { base64ToBlobData } from './image-handler/index' const DEF_OPTIONS = { // 内容是否可以被编辑 editable: true, // 编辑器输入内容绝对定位 fixed: false, // editor min height // minHeight: window.innerHeight, // style placeholder: 'Enter...', placeholderColor: '', lineHeight: 1.5, // paragraph tail spacing, default 10px paragraphTailSpacing: '', cursorColor: '', textColor: '', // iphone会自动移动,难控制 cursorOffsetTop: 30, // 自定义粘贴处理 customPasteHandler: false, /** * ****************************** * toolbar options * ****************************** */ // Has the toolbar been fixed? toolbarBeenFixed: true, toolbarHeight: 50, // buttons name, and order toolbarBtns: ['select-picture', 'text-style'], /** * ****************************** * image options * ****************************** */ // customize Picture Handler customPictureHandler: false, // image max width imageMaxWidth: 720, // image max size, unit Kib, default 20M imageMaxSize: 20480, // template imageSectionTemp: `<section class="child-is-picture"><img src="{url}"></section>`, // GIF pictures are not processed ignoreGif: true, // Force the width/height of the picture, even if the width/height of the picture // is smaller than the target width/height forceImageResize: false, /** * ****************************** * text style options * ****************************** */ // text style, value ['#333', '#f00', ...] textStyleColors: null, textStyleTitle: 'Set Style', textStyleHeadLeftBtnText: 'Clear style', textStyleHeadAlign: 'center', /** * ****************************** * color options * ****************************** */ mainColor: '', // border color borderColor: '' } function ZxEditor (selector, _options) { if (!this instanceof ZxEditor) { throw new Error('ZxEditor is a constructor and should be called with the `new` keyword') } /** * *************************************************** * check selector * *************************************************** */ this.$wrapper = $(selector) if (!this.$wrapper[0]) { throw new Error(`Can't found '${selector}' Node in document!`) } // version this.version = '__VERSION__' // ZxQuery instance this.$ = $ this.ExpansionPanel = ExpansionPanel // options this.options = Object.assign(DEF_OPTIONS, _options) this.init(this.options) } ZxEditor.prototype = { constructor: ZxEditor, init (options) { options = options || this.options /** * *************************************************** * event listeners * or expansionPanel instance * *************************************************** */ // $().on, $().off, $().trigger this.$eventHandlers = {} // this.on, this.off, this.emit this.customEvents = {} // extend prototype extendPrototypes(ZxEditor) // expansionPanel instances this.expansionPanels = [] /** * *************************************************** * create dom * *************************************************** */ initDom.call(this, options) /** * *************************************************** * style and placeholder * *************************************************** */ initStyle.call(this, options) /** * *************************************************** * cursor * *************************************************** */ this.cursor = new CursorClass(this.$content) this.$cursorNode = this.cursor.getCurrentNode() /** * *************************************************** * event: last * *************************************************** */ handleEvents.call(this) }, /** * 插入元素或字符串 * @param el */ insertElm (el) { // string if (!el) return // 光标元素及偏移量 let $cursorNode = this.$cursorNode let newRangeEl, newRangeOffset /** * string */ if (typeof el === 'string') { // 光标所在元素内容为空 if ($cursorNode.isEmpty()) { $cursorNode.text(el) newRangeEl = $cursorNode newRangeOffset = el.length } else if ($cursorNode.children().every($item => $item.isTextNode())) { let rangeOffset = this.cursor.offset let rangeNodeStr = $cursorNode.text() let tmpStr = rangeNodeStr.substr(0, rangeOffset) + el + rangeNodeStr.substr(rangeOffset) // $section = $cursorNode.closest('section') $cursorNode.text(tmpStr) newRangeEl = $cursorNode newRangeOffset = el.length + rangeOffset } else { // 创建一个section let $newEl = $(`<section>${el}</section>`) // 插入到childIndex后 $newEl.insertAfter($cursorNode) newRangeEl = $newEl newRangeOffset = el.length } } /** * 插入元素为:非文本 */ else { let $el = $(el) let $elm for (let i = 0; i < $el.length; i++) { $elm = $($el[i]) let nodeName = $elm.nodeName() // SECTION if (nodeName !== 'section') { if ($elm.nodeType() === 1 && !/video|img|audio/.test(nodeName)) { $elm.changeNodeName('section') } else { let $tmp = $(`<section></section>`) $elm = $tmp.append($elm) } } if ($cursorNode.isEmpty()) { // siblings is empty if ($cursorNode.next()[0] && $cursorNode.next().isEmpty()) { $cursorNode.replace($elm) } else { $elm.insertBefore($cursorNode) } } else { $elm.insertAfter($cursorNode) } // 判断$el是否有下一个节点,有:光标指向el结束,无:则插入空行,并移动光标 let next = $elm.next()[0] if (next) { newRangeEl = $elm newRangeOffset = $elm.isTextNode() ? $elm.text().length : 0 } else { let $section = $(`<section><br></section>`) this.$content.append($section) newRangeEl = $section newRangeOffset = 0 } } } this._checkChildSection() this.$content.trigger('input') // console.log(newRangeEl, newRangeOffset) this.cursor.setRange(newRangeEl, newRangeOffset) }, /** * 插入空行 */ insertBlankLine () { let $el = $(`<section><br></section>`) this.insertElm($el) this.cursor.setRange($el, 0) }, /** * 检查内容是否为空 * @private */ _checkEmpty () { let $el = this.$content if ($el.isEmpty()) { $el.addClass('is-empty') } else if ($el.hasClass('is-empty')) { $el.removeClass('is-empty') } }, /** * 检查一级子元素,nodeName是否为(SECTION|H1|H2|H3|H4|BLOCKQUOTE|UL) * 否:则替换为section标签,或者放入section标签内 * @private */ _checkChildSection () { if (!this.$cursorNode) this.$cursorNode = this.cursor.getCurrentNode() let cursorNode = this.$cursorNode[0] let isCursorNode = false const parent = this.$content[0] let childNodes = parent.childNodes let el for (let i = 0; i < childNodes.length; i++) { el = childNodes[i] if (el.nodeType === 1) { if (!/SECTION|H1|H2|H3|H4|BLOCKQUOTE|UL/.test(el.nodeName)) { isCursorNode = el === cursorNode el = util.changeNodeName(el, 'section') if (isCursorNode) { this.$cursorNode = $(el) this.cursor.setRange(el) } } } else { let $tmp = $(`<section></section>`) $tmp.append(el.cloneNode()) parent.replaceChild($tmp[0], el) this.$cursorNode = $tmp this.cursor.setRange($tmp) } } }, /** * 清空内容 */ remove () { this.setHtml() }, /** * 设置编辑器内容 * @param html */ setHtml (html) { this.$content.html(html || '<section><br></section>') this._checkChildSection() this.cursor.setRange(this.$content.firstChild(), 0) this.$content.trigger('input') }, /** * 获取编辑器html内容 * 返回html内容 * @return {*} */ getHtml () { return this.$content.html() }, /** * 获取编辑器文本内容, * 以纯文本形式返回数据 * @return {*|string} */ getText () { return this.$content.text() }, /** * destroy event and Node */ destroy (){ let evt // remove $events for (let key in this.$eventHandlers) { evt = this.$eventHandlers[key] evt.$target.off(evt.type, evt.handler, evt.capture) delete this.$eventHandlers[key] } // remove customEvents for (let key in this.customEvents) { evt = this.customEvents[key] this.off(key) } // other object this.cursor = null this.toolbar = null this.textStylePanel = null // other ExpansionPanel for (let key in this) { if (this[key] instanceof ExpansionPanel) { this[key] = null } } // Node this.$editor.remove() }, /** * set content height * default minHeight is window innerHeight, marginBottom * @param data */ setContentHeight (data) { let winHeight = window.innerHeight let styles = { // 防止正文内容被键盘挡住,无法查看 marginBottom: winHeight + 'px' } // check height if (data.height) { styles.height = typeof data.height === 'number' ? (data.height + 'px') : data.height } else { styles.minHeight = (util.int(data.minHeight) || winHeight) + 'px' } this.$content.css(styles) }, /** * get base64 images from this.$content * @returns {Array} */ getBase64Images () { const arr = [] const $imgs = this.$content.find('img') 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: base64ToBlobData(base64) }) } } return arr }, /** * 设置指定id图片src * @param id * @param src * @returns {boolean} */ setImageSrc (id, src) { let img = this.$content.find('#' + id)[0] if (img) { img.src = src img.removeAttribute('id') return true } return false }, /** * plugin * @param fn */ plugin (fn) { if (typeof fn === 'function') { fn.call(this) } }, /** * check cursor position */ checkPosition () { const $el = this.$cursorNode = this.cursor.getCurrentNode() // 当前光标位置 let cursorOffset = this.cursor.offset // 文本内容长度 let len = $el.text().length // 当前元素高度 let height = $el.height() // 当前元素top let top = $el.offset().top let scrollTop // 当前光标位置,距当前元素顶部距离 let cursorHeight = 0 // 每行大概有几个字 let textNumOfPerLine = len / (height / this.lineHeight) // 当前光标所在行 let line = height > this.lineHeight ? Math.floor(cursorOffset / textNumOfPerLine) : 1 // 当前光标位置,至当前元素顶部高度 - 1行高,防止移动后,光标位置太贴近顶部 let cursorHeightInCurrentNode = (line - 1) * this.lineHeight // editor postion: fixed; if (this.options.fixed) { } else { // 当前光标位置超过了屏幕的4分之1 if (cursorHeightInCurrentNode > window.innerHeight / 4) { cursorHeight = cursorHeightInCurrentNode } scrollTop = $(window).scrollTop() $(window).scrollTop(scrollTop + top + cursorHeight - this.options.cursorOffsetTop) } } } export default ZxEditor