sp-editor
Version:
SpEditor is a HTML5 rich text editor in smartphone browsers
184 lines (168 loc) • 5.53 kB
text/typescript
/**
* Created by Capricorncd.
* https://github.com/capricorncd
* Date: 2022/05/14 23:21:21 (GMT+0900)
*/
import { Editor, ALLOWED_NODE_NAMES } from '@sp-editor/editor'
import { StylePanel } from '@sp-editor/style-panel'
import { Toolbar, ButtonOptions } from '@sp-editor/toolbar'
import { AnyObject } from '@sp-editor/types'
import { handleImageFile, MediaFileHandlerData } from 'image-process'
import { $, createElement, slice } from 'zx-sml'
import { SpEditorOptions, DEF_OPTIONS } from './options'
import './style.scss'
/**
* @document SpEditor
* SpEditor is a HTML5 rich text editor in smartphone browsers, and it's extends [Editor](./Editor.md).
*
* ```js
* import { SpEditor } from 'sp-editor'
* import 'sp-editor/css'
*
* const spEditor = new SpEditor({
* // container: document.querySelector('#app'),
* // or
* container: '#app',
* })
* ```
*/
export class SpEditor extends Editor {
private readonly $el: HTMLElement
private readonly stylePanel: StylePanel
private readonly toolbar: Toolbar
private fileInput: HTMLInputElement | null = null
private _inputChangeHandler: (e: Event) => void
constructor(selector: string | HTMLElement | Partial<SpEditorOptions>, options: Partial<SpEditorOptions> = {}) {
let container: HTMLElement | null = null
// check selector
if (typeof selector === 'string' || selector instanceof HTMLElement) {
container = $(selector) as HTMLElement
} else {
options = selector || {}
if (typeof options.container === 'string') container = $(options.container) as HTMLElement
}
options = {
...DEF_OPTIONS,
...options,
}
if (!container) {
throw new Error(`Can't found '${selector}' Node in document!`)
}
const $el = createElement('div', { class: 'sp-editor' })
super({
...options,
container: $el,
})
container.append($el)
this.$el = $el
this.stylePanel = new StylePanel(options)
this.use(this.stylePanel, this.$el)
this.toolbar = new Toolbar(options)
this.use(this.toolbar, this.$el)
// handle picture
this._inputChangeHandler = (e: Event) => {
const el = e.currentTarget as HTMLInputElement
this.handleImageFile(el.files)
.then((items) => {
items.forEach((item) => {
const ignoreGif = /gif$/i.test(item.raw.type) && options.ignoreGif
this.insert(`<img src="${ignoreGif ? item.raw.data : item.data}">`)
})
})
.catch((err) => {
this.emit('error', err)
})
}
this.on('toolbarButtonOnClick', (name) => {
switch (name) {
case 'choose-picture':
if (typeof options.customPictureHandler === 'function') {
options.customPictureHandler()
} else {
if (!this.fileInput) {
const attrs: AnyObject = {
type: 'file',
style: {
display: 'none',
},
accept: options.chooseFileAccept,
}
if (options.chooseFileMultiple) attrs.multiple = true
this.fileInput = createElement<HTMLInputElement>('input', attrs)
this.$el.append(this.fileInput)
this.fileInput.addEventListener('change', this._inputChangeHandler)
this.fileInput.click()
} else {
this.fileInput.click()
}
}
break
case 'text-style':
this.stylePanel.show()
break
}
})
}
/**
* @method handleImageFile(files)
* Image files handler.
* @param files `FileList | File[] | Blob[] | null` Image files.
* @returns `Promise<MediaFileHandlerData[]` [MediaFileHandlerData](https://github.com/capricorncd/image-process-tools#returns)
*/
handleImageFile(files: FileList | File[] | Blob[] | null): Promise<MediaFileHandlerData[]> {
if (!files) return Promise.resolve([])
return new Promise((resolve, reject) => {
Promise.all(slice<File, FileList | File[] | Blob[]>(files).map(this._handleFile))
.then((res) => {
resolve(res.sort((a, b) => a.index - b.index).map((item) => item.data))
})
.catch(reject)
})
}
private _handleFile(file: File | Blob, index: number): Promise<{ data: MediaFileHandlerData; index: number }> {
return new Promise((resolve, reject) => {
handleImageFile(file)
.then((data) => {
resolve({
data,
index,
})
})
.catch(reject)
})
}
/**
* @method addToolbarButton(params, index)
* Add a custom button to `toolbar`.
* @param params `ButtonOptions` [ButtonOptions](#ButtonOptions)
* @param index? `number` New button insertion index.
* ```js
* // Add a button named 'custom-button-name' for toolbar.
* editor.addToolbarButton({
* name: 'custom-button-name',
* })
*
* // when the button is clicked
* editor.on('toolbarButtonOnClick', (name) => {
* if (name === 'custom-button-name') {
* // do something ...
* }
* })
* ```
*/
addToolbarButton(params: ButtonOptions, index?: number) {
this.toolbar.addButton(params, index)
}
/**
* @method destroy()
* destroy events
*/
destroy(): void {
super.destroy()
this.stylePanel.destroy()
this.toolbar.destroy()
this.fileInput?.removeEventListener('change', this._inputChangeHandler)
}
}
export { ALLOWED_NODE_NAMES }
export type { SpEditorOptions }