import get from 'lodash.get'
import assign from 'object-assign'
import CabinetBase, { emitter } from './base'
import './document.css'
import { events, ReadyState } from './constants'
const historyContainerTemplate = `
`
const revisionContainerTemplate = `
`
export default class ShimoDocumentCabinet extends CabinetBase {
public editor: ShimoSDK.Document.Editor
public plugins: {
demoScreen?: ShimoSDK.Document.DemoScreen
collaboration?: ShimoSDK.Common.Collaboration
collaborator?: ShimoSDK.Document.Collaborator
comment?: ShimoSDK.Document.Comment
gallery?: ShimoSDK.Document.Gallery
history?: ShimoSDK.Document.History
tableOfContent?: ShimoSDK.Document.TableOfContent
uploader?: ShimoSDK.Document.Uploader
shortcut?: ShimoSDK.Document.Shortcut
revision?: ShimoSDK.Document.Revision
mobile?: ShimoSDK.Document.Mobile
}
public getPlugin: (name: string) => Promise
private sdkCommon: any
private sdkDocument: any
private user: ShimoSDK.User
private editorOptions: ShimoSDK.Document.EditorOptions
private file: ShimoSDK.File
private token: string
private _commentShowCount: number
private onError: (error: any) => void
private async?: boolean
protected pluginOptions: ShimoSDK.Document.Plugins
protected emitter: emitter
constructor (options: {
element: HTMLElement
sdkDocument: any
sdkCommon: any
user: ShimoSDK.User
entrypoint: string
token: string
file: ShimoSDK.File
editorOptions: ShimoSDK.Document.EditorOptions
availablePlugins: string[]
onError?: (error: any) => void
getPlugin: (name: string) => Promise
async?: boolean
emitter?: emitter
}) {
super(options.element)
this.sdkCommon = options.sdkCommon
this.sdkDocument = options.sdkDocument
this.user = options.user
this.editorOptions = Object.assign({}, options.editorOptions, {
editable: options.file.permissions?.editable,
readable: options.file.permissions?.readable,
commentable: options.file.permissions?.commentable
})
this.plugins = {}
this.file = options.file
this.entrypoint = options.entrypoint
this.token = options.token
this.availablePlugins = options.availablePlugins
this.pluginOptions = this.preparePlugins(
options.editorOptions.plugins,
{
Revision: false
},
plugin => {
if (plugin === 'mobile') {
return !!this.editorOptions.isMobile
}
return true
}
)
this._commentShowCount = 0
this.getPlugin = options.getPlugin
this.async = options.async
if (typeof options.onError === 'function') {
this.onError = options.onError
} else {
this.onError = err => { throw err }
}
if (typeof options.emitter === 'function') {
this.emitter = options.emitter
}
}
public async render () {
const editor = this.initEditor()
let localeConfig: {
fetchLocaleSync?: string;
locale?: string;
} = {}
if (this.editorOptions.localeConfig) {
localeConfig = this.editorOptions.localeConfig
}
const editorScroller = this.getElement(undefined, 'div', { id: 'sm-editor-scroller' })
this.element.appendChild(editorScroller)
editorScroller.classList.add('sm-editor-scroller')
const toolbarOptions = this.getToolbarOptions()
if (typeof toolbarOptions === 'object') {
editorScroller.addEventListener('scroll', () => {
if (editorScroller.scrollTop === 0) {
toolbarOptions.parent.classList.remove('active')
} else {
toolbarOptions.parent.classList.add('active')
}
})
}
const editorElm = this.getElement(undefined, 'div', { id: 'sm-editor' })
editorElm.classList.add('sm-editor')
editor.render(
editorScroller.appendChild(editorElm),
{
readOnly: !this.editorOptions.editable,
id: this.user.id,
localeConfig,
modules: {
toolbar: toolbarOptions
},
scrollingContainer: editorScroller
}
)
editor.setContent(this.file.content)
this.emitter(events.readyState, { [events.readyState]: ReadyState.editorReady })
const p = Promise
.all(this.availablePlugins.map(p => this.initPlugin(editor, p)))
.then(() => {
this.initCollaboration(editor)
if (this.editorOptions.isMobile) {
document.body.classList.add('in-mobile')
editorElm.classList.add('in-mobile')
editorScroller.classList.add('in-mobile')
this.initMobile(editor, this.element, editorElm)
}
})
.catch(err => this.onError(err))
.then(() => {
this.emitter(events.readyState, { [events.readyState]: ReadyState.pluginReady })
this.emitter(events.readyState, { [events.readyState]: ReadyState.allReady })
})
.catch(err => this.onError(err))
if (!this.async) {
await p
}
this.editor = editor
return editor
}
private async initPlugin (editor: ShimoSDK.Document.Editor, plugin: string) {
if (plugin in this.pluginOptions === false) {
return
}
if (plugin !== 'Collaboration' && plugin !== 'Toolbar') {
await this.getPlugin(plugin)
}
const method = `init${plugin}`
if (typeof this[method] === 'function') {
if (plugin !== 'Mobile' && plugin !== 'Collaboration') {
this[method](editor)
}
}
}
public destroy (): void {
const destroy = () => {
this.editor.destroy()
for (const k in this.plugins) {
if (this.plugins[k] && typeof this.plugins[k].destroy === 'function') {
this.plugins[k].destroy()
}
}
}
if (this.editor.isReady) {
destroy()
} else {
this.editor.on('ready', destroy)
}
}
public initEditor (): ShimoSDK.Document.Editor {
const options: ShimoSDK.Document.EditorOptions = {
id: this.user.id,
readOnly: !this.editorOptions.editable,
editable: this.editorOptions.editable,
commentable: this.editorOptions.commentable
}
return new this.sdkDocument.Editor(options)
}
public initMobile (editor: ShimoSDK.Document.Editor, container: HTMLElement, editorElm: HTMLElement): void {
if (this.editorOptions.isMobile) {
const options = assign(
{
commentable: this.editorOptions.commentable
},
this.pluginOptions.Mobile,
{
editor,
container,
editorWrap: editorElm,
comment: this.plugins.comment,
toolbar: true
}
) as ShimoSDK.Document.MobileOptions
this.plugins.mobile = new this.sdkDocument.plugins.Mobile(options)
}
}
public initGallery (editor: ShimoSDK.Document.Editor): void {
if (this.pluginOptions.Gallery === false) {
return
}
const options: ShimoSDK.Document.GalleryOptions = assign(
{},
this.pluginOptions.Gallery,
{
editor
}
)
const gallery: ShimoSDK.Document.Gallery = new this.sdkDocument.plugins.Gallery(options)
this.plugins.gallery = gallery
gallery.render()
}
public initHistory (editor: ShimoSDK.Document.Editor): void {
if (this.pluginOptions.History === false || this.editorOptions.isMobile) {
return
}
const options = assign(
{},
this.pluginOptions.History,
{
editor,
guid: this.file.guid,
service: {
fetch: `${this.entrypoint}/files/${this.file.guid}/` +
`histories?accessToken=${this.token}`,
revert: `${this.entrypoint}/files/${this.file.guid}/revert?accessToken=${this.token}`,
user: `${this.entrypoint}/users?accessToken=${this.token}`
}
}
) as ShimoSDK.Document.HistoryOptions
const history: ShimoSDK.Document.History = new this.sdkDocument.plugins.History(options)
this.plugins.history = history
let historyContainer = this.getElement(get(this.pluginOptions, 'History.container'))
if (!historyContainer) {
this.element.insertAdjacentHTML('afterend', historyContainerTemplate)
historyContainer = document.querySelector('.sm-history-container') as HTMLElement
}
const historyElement = this.getElement(undefined, 'div', { id: 'sm-history' })
historyContainer.appendChild(historyElement)
history.render(historyElement)
const historyButton = this.getElement('#ql-history', 'button', { id: 'ql-history', type: 'button' })
historyButton.classList.add('ql-history')
if (!historyButton.textContent && !historyButton.dataset.label) {
historyButton.dataset.label = '历史'
}
const toolbarGroup = document.querySelector('.ql-toolbar-default')!.querySelectorAll('.ql-formats')
const toolbarContainer = toolbarGroup[toolbarGroup.length - 1]
if (toolbarContainer) {
toolbarContainer.appendChild(historyButton)
}
historyButton.addEventListener('click', () => {
historyContainer!.style.display = 'block'
history.show()
editor.comment.hide()
})
document.querySelector('.sm-history-close-btn')!.addEventListener('click', () => {
historyContainer!.style.display = 'none'
editor.comment.show()
})
}
public initTableOfContent (editor: ShimoSDK.Document.Editor): void {
if (this.pluginOptions.TableOfContent === false || this.editorOptions.isMobile) {
return
}
const options: ShimoSDK.Document.TableOfContentOptions = assign(
{},
this.pluginOptions.TableOfContent,
{
editor
}
)
const tableOfContent: ShimoSDK.Document.TableOfContent = new this.sdkDocument.plugins.TableOfContent(options)
this.plugins.tableOfContent = tableOfContent
if (options.container instanceof HTMLElement) {
options.container.classList.add('table-of-content')
}
tableOfContent.render(options.container)
}
public initCollaboration (editor: ShimoSDK.Document.Editor): void {
let collaborators: ShimoSDK.Document.Collaborator | undefined
if (this.pluginOptions.Collaborator !== false) {
const collaboratorOptions: ShimoSDK.Document.CollaboratorOptions = assign(
{
service: {
user: `${this.entrypoint}/users?accessToken=${this.token}`
},
avatarTrack: false,
cursorTrack: false
},
this.pluginOptions.Collaborator,
{
editor,
user: this.user
}
)
collaborators = new this.sdkDocument.plugins.Collaborator(collaboratorOptions)
this.plugins.collaborator = collaborators
}
if (this.pluginOptions.Collaboration === false) {
return
}
const collaborationOptions: ShimoSDK.Common.CollaborationOptions = assign(
{
pullUrl: `${this.entrypoint}/files/${this.file.guid}/pull?accessToken=${this.token}`,
composeUrl: `${this.entrypoint}/files/${this.file.guid}/compose?accessToken=${this.token}`,
selectUrl: `${this.entrypoint}/files/${this.file.guid}/select?accessToken=${this.token}`,
offlineEditable: false
},
this.pluginOptions.Collaboration,
{
editor,
type: 'richdoc',
rev: this.file.head,
guid: this.file.guid,
collaborators
}
)
const collaboration: ShimoSDK.Common.Collaboration = new this.sdkCommon.Collaboration(collaborationOptions)
this.plugins.collaboration = collaboration
if (typeof collaborationOptions.onSaveStatusChange === 'function') {
collaboration.on('saveStatusChange' as ShimoSDK.Common.CollaborationEvents, collaborationOptions.onSaveStatusChange)
}
collaboration.start()
this.promptIfHasUnsavedChanges(collaboration)
if (collaborators) {
collaborators.render(collaboration)
}
}
public initComment (editor: ShimoSDK.Document.Editor): void {
if (this.pluginOptions === false) {
return
}
const options: ShimoSDK.Document.CommentOptions = assign(
{
service: {
fetch: `${this.entrypoint}/files/${this.file.guid}/comments?accessToken=${this.token}`,
create: `${this.entrypoint}/files/${this.file.guid}/comments?accessToken=${this.token}`,
delete: `${this.entrypoint}/files/${this.file.guid}/comments/{commentGuid}?accessToken=${this.token}`,
close: `${this.entrypoint}/files/${this.file.guid}/` +
`comments/close/{selectionGuid}?accessToken=${this.token}`
},
mentionable: false
},
this.pluginOptions.Comment,
{
editor,
user: this.user,
unreadEnable: false
}
)
if (this.editorOptions.isMobile) {
options.hasPanel = false
options.hasGenerator = false
}
const comment: ShimoSDK.Document.Comment = new this.sdkDocument.plugins.Comment(options)
this.plugins.comment = editor.comment = comment
comment.render()
comment.show()
}
public initDemoScreen (editor: ShimoSDK.Document.Editor) {
if (this.pluginOptions.DemoScreen === false || this.editorOptions.isMobile) {
return
}
const options: ShimoSDK.Document.DemoScreenOptions = { editor }
const demoScreen: ShimoSDK.Document.DemoScreen = new this.sdkDocument.plugins.DemoScreen(options)
this.plugins.demoScreen = demoScreen
}
public initUploader (editor: ShimoSDK.Document.Editor) {
if (this.pluginOptions.Uploader === false) {
return
}
const uploadConfig: { [key: string]: any } = assign({}, this.pluginOptions.Uploader)
const options: ShimoSDK.Document.UploaderOptions = {
editor,
container: '#sm-editor',
url: uploadConfig.origin,
accessToken: uploadConfig.token,
type: uploadConfig.server
}
const uploader = new this.sdkDocument.plugins.Uploader(options)
this.plugins.uploader = uploader
}
public initShortcut (editor: ShimoSDK.Document.Editor): void {
if (this.pluginOptions.Shortcut === false || this.editorOptions.isMobile) {
return
}
const options: ShimoSDK.Document.ShortcutOptions = {
editor,
plugins: {
demoScreen: undefined,
revision: undefined,
history: undefined,
tableOfContent: undefined
}
}
const shortcut: ShimoSDK.Document.Shortcut = new this.sdkDocument.plugins.Shortcut(options)
this.plugins.shortcut = shortcut
shortcut.render()
}
public initRevision (editor: ShimoSDK.Document.Editor): void {
if (this.editorOptions.isMobile) {
return
}
const pluginOptions = this.pluginOptions.Revision
if (
pluginOptions !== true &&
/* tslint:disable-next-line:strict-type-predicates */
(typeof pluginOptions !== 'object' || pluginOptions == null)
) {
return
}
const options = assign(
{
service: {
length: `${this.entrypoint}/files/${this.file.guid}/revisions?accessToken=${this.token}`,
fetch: `${this.entrypoint}/files/${this.file.guid}/revisions?accessToken=${this.token}`,
fetchTitle: `${this.entrypoint}/files/${this.file.guid}/histories/{historyId}/title?accessToken=${this.token}`,
postRename: `${this.entrypoint}/files/${this.file.guid}/revisions/{id}?accessToken=${this.token}`,
revert: `${this.entrypoint}/files/${this.file.guid}/revert?accessToken=${this.token}`,
generate: `${this.entrypoint}/files/${this.file.guid}/revisions?accessToken=${this.token}`,
delete: `${this.entrypoint}/files/${this.file.guid}/revisions/{id}?accessToken=${this.token}`,
fetchContent: `${this.entrypoint}/files/${this.file.guid}/histories/{id}?accessToken=${this.token}`
}
},
pluginOptions,
{
editor
}
) as ShimoSDK.Document.RevisionOptions
const revision: ShimoSDK.Document.Revision = new this.sdkDocument.plugins.Revision(options)
this.plugins.revision = revision
this.element.insertAdjacentHTML('afterend', revisionContainerTemplate)
const container = document.querySelector('.sm-revision-container') as HTMLElement
container.querySelector('.icon-close')!.addEventListener('click', () => {
container.style.display = 'none'
this.setCommentShowStatus(true)
})
if (!options.disableDefaultButtons) {
const parent = document.querySelector('.ql-toolbar-default')
if (parent) {
const buttonGroups = document.createElement('span')
buttonGroups.className = 'ql-formats ql-revision'
const html = `
`
buttonGroups.insertAdjacentHTML('afterbegin', html)
parent.appendChild(buttonGroups)
const toggleButtons = show => {
const elm = buttonGroups.querySelector('.ql-revision')
if (elm) {
const method = show ? 'add' : 'remove'
elm.classList[method]('ql-expanded')
if (elm.nextElementSibling) {
elm.nextElementSibling.classList[method]('ql-expanded')
}
}
}
buttonGroups.querySelector('button.ql-revision')!.addEventListener('click', function () {
toggleButtons(!(this as HTMLElement).classList.contains('ql-expanded'))
})
const showRev = document.querySelector('.ql-view-revision') as HTMLElement
showRev.addEventListener('click', () => {
const renderContainer = container.querySelector('.sm-sidebar-body') as HTMLElement
if (renderContainer.innerHTML === '') {
revision.render(renderContainer)
}
container.style.display = 'block'
this.setCommentShowStatus(false)
toggleButtons(false)
})
const saveRev = document.querySelector('.ql-create-revision') as HTMLElement
saveRev.addEventListener('click', () => {
revision.save('版本保存成功')
toggleButtons(false)
})
}
}
revision.render()
}
private setCommentShowStatus (show: boolean) {
if (!this.plugins.comment) {
return
}
this._commentShowCount += show ? -1 : 1
if (this._commentShowCount < 0) {
this._commentShowCount = 0
}
if (this._commentShowCount > 0) {
this.plugins.comment.hide()
} else {
this.plugins.comment.show()
}
}
private getToolbarOptions () {
let container: HTMLElement | null
if (!this.pluginOptions.Toolbar) {
return false
}
container = this.getElement((this.pluginOptions.Toolbar as ShimoSDK.Document.ToolbarOptions).container)
if (!container) {
container = this.getElement(undefined, 'div', { id: 'sm-toolbar' })
this.element.insertBefore(container, this.element.firstChild)
}
container.classList.add('sm-toolbar')
return { parent: container }
}
}