UNPKG

soocrate-core

Version:

this is the core of soocrate application

480 lines (420 loc) 13.5 kB
import { Comments } from '../view/Comments' import { EventEmitter } from 'events' import { QuillManager } from './QuillManger' import { MarkerViewManager } from './MarkerViewManager' var debug = require('debug')('crate:view:editor') /** * EditorController this the link between the core functions and the interface. */ export class EditorController extends EventEmitter { /** * [constructor description] * @param {[doc]} document this is the object that contains all proprieties of a document. * @param {[string]} sessionID [description] */ constructor(document, sessionID, editorContainerID) { super() /** * this is the object that contains all proprieties of a document. * @type {[doc]} */ this._document = document /** * ViewEditor the used editor, here it is Quill editor * @see https://quilljs.com/ * @type {Quill} */ this._editorContainerID = editorContainerID this._comments = {} this._sessionID = sessionID //if it is true the events will not be disseminated this._silent = false } /** * * @param {*} silent if it is true the events will not be disseminated, else the events will be treated */ silent(silent) {} startSilence() { this._silent = true } stopSilence() { setTimeout(() => { this._silent = false }, 0) } /** * loadDocument load the document if it exist in the local storage * @param {string} sessionID the session ID of the document * @return {[type]} [description] */ loadDocument() { this._comments = new Comments() this._quillManager = new QuillManager( this._editorContainerID, this._comments, this ) this.viewEditor = this._quillManager.getQuill() } initDocument() { this.loadDocument() this.initCommunicationModules() this.loadLocalContent(this._sessionID) this._comments .init(this) .addAuthorInformation() .UpdateComments() this.startEventListeners() } initCommunicationModules() { this.markerManager = this._document._communication.markerManager this.textManager = this._document._communication.textManager this._MarkerViewManager = new MarkerViewManager(this.markerManager, this) } /** * load content from localStorage if it exist * @param {*} sessionID */ loadLocalContent() { if (store.get('CRATE2-' + this._sessionID)) { const doc = store.get('CRATE2-' + this._sessionID) this.viewEditor.setContents(doc.delta, 'user') jQuery(`#${this._editorContainerID} #title`).text(doc.title) this.convertLocalLinks() // this is to convert the links to inside links } } /** * Start all the event listeners related to the editor */ startEventListeners() { // Local events this.viewEditor.on('selection-change', (range, oldRange, source) => { if (range) { this.markerManager.caretMoved(range) } }) this.viewEditor.on('text-change', (delta, oldDelta, source) => { this.textChange(delta, oldDelta, source) this.emit('thereAreChanges') }) // Remote events this.textManager._insertManager.on('remoteInsert', (element, indexp) => { this.remoteInsert(element, indexp) this.emit('thereAreChanges') }) this.textManager._removeManager.on('remoteRemove', index => { this.remoteRemove(index) this.emit('thereAreChanges') }) //At the reception of Title changed operation this.textManager._titleManager.on('changeTitle', title => { jQuery(`#${this._editorContainerID} #title`).text(title) this.emit('thereAreChanges') }) } /** * textChange description * @param {[type]} delta [description] * @param {[type]} oldDelta [description] * @param {[type]} source [description] * @listens editor content changes * @return {[type]} [description] */ textChange(delta, oldDelta, source) { if (!this._silent) { this.applyChanges(this.getDeltaWithoutRetainWithAttributes(delta), 0) } } /** * applyChanges Send delta object with attributes character by character starting from the position "iniRetain" ] * @param {[type]} delta [description] * @param {[type]} iniRetain [description] * @return {[type]} [description] */ applyChanges(delta, iniRetain) { let start = iniRetain let Operations = this.getOperations(delta) Operations.map(operation => { start = this.sendIt(operation, start, false) }) } /** * this function converts retain with attributes to delete and insert, this is because Lseq ({@link https://github.com/Chat-Wane/LSEQ}) * dose not support updating, thus UPDATE=DELETE+INSERT. * For more information about Delta Object, see the following link: {@link https://github.com/quilljs/delta} * @param {*} delta */ getDeltaWithoutRetainWithAttributes(delta) { const ops = delta.ops let index = 0 const newOPS = ops.reduce((newOps, op, curr) => { if (op.retain && !op.attributes) { newOps.push(op) index += op.retain } else if (op.retain) { if (curr === 0) { newOps.push({ retain: index }) } newOps.push({ delete: op.retain }) const delta = this.getDelta(index, index + op.retain) newOps.push.apply(newOps, delta.ops) index += op.retain } else { newOps.push(op) } return newOps }, []) return { ops: newOPS } } /** * sendIt Send the changes character by character * @param {[type]} text [description] * @param {[type]} operation.Attributes [description] * @param {[type]} start [description] * @param {[type]} operation.Value [description] * @param {[type]} operation.Name [description] * @param {[type]} start [description] * @param {Boolean} isItInsertWithAtt [description] * @return {[type]} [description] */ sendIt(operation, start, isItInsertWithAtt) { switch (operation.Name) { case 'insert': this.sendInsert(start, operation) break case 'delete': this.sendDelete(start, operation, isItInsertWithAtt) break } return this.getNextIndex(start, operation) } getNextIndex(index, operation) { debug('getNextIndex', { index, operation }) let nextIndex = index if (operation.Name === 'insert' && operation.Type === 'text') { nextIndex = index + operation.Value.length } if (operation.Name === 'insert' && operation.Type != 'text') { nextIndex = index + 1 } if (operation.Name === 'retain' && operation.Attributes === '') { nextIndex = index + operation.Value } debug('getNextIndex', { nextIndex }) return nextIndex } /** * Is it Line operation: 'blockquote','header','indent','list','align','direction','code-block' * @param {*} Attributes */ isItBlock(Attributes) { const lineOps = [ 'blockquote', 'header', 'indent', 'list', 'align', 'direction', 'code-block' ] let propertyNames = Object.getOwnPropertyNames(Attributes) const f = propertyNames.reduce((found, propertyName) => { if (lineOps.indexOf(propertyName) >= 0) { return found || true } else { return found || false } }, false) return f } /** sometimes we receive the attributes one by one if they are not complete yet we wait * we have to specify the first attributes that appears and the their number */ isItComplete(attributes) { if ( attributes.hasOwnProperty('commentAuthor') && Object.keys(attributes).length < 5 ) { return false } return true } sendInsert(index, Operation) { if (Operation.Type === 'text') { this.sendCharByChar(Operation.Value, index) } else { this.insert(Operation.Type, Operation.Value, index) } return index + Operation.Length } sendCharByChar(text, index) { for (let i = index; i < index + text.length; ++i) { debug('send [%]', text[i - index]) this.insert('char', text[i - index], i) } } sendDelete(index, operation, isItInsertWithAtt) { debug('Send delete', index, operation, isItInsertWithAtt) //to ensure that the editor contains just \n without any attributes if (!isItInsertWithAtt) { this._comments.UpdateComments() } // Delete character by character for (var i = index; i < index + operation.Length; ++i) { this.textManager._removeManager.remove(index) } } insert(type, content, position) { const attributes = this.viewEditor.getFormat(position, 1) const packet = { type, content, attributes } this.textManager._insertManager.insert({ packet, position }) if (Object.keys(attributes).length > 0) { this.updateCommentsLinks() } } /** * remoteInsert At the reception of insert operation * @param {[type]} element [description] * @param {[type]} indexp [description] * @return {[type]} [description] */ remoteInsert(element, indexp) { var index = indexp - 1 debug('Remote Insert : ', element, index) if (index !== -1) { if (element.type === 'char') { this.viewEditor.insertText( index, element.content, element.attributes, 'silent' ) if (element.content != '\n') { this.viewEditor.removeFormat(index, 1, 'silent') } } else { this.viewEditor.insertEmbed( index, element.type, element.content[element.type], 'silent' ) } if (element.attributes) { if (element.text != '\n') { this.viewEditor.formatLine(index, element.attributes, 'silent') this.viewEditor.formatText(index, 1, element.attributes, 'silent') } this.updateCommentsLinks() } } } /** * remoteRemove At the reception of remove operation * @param {[type]} index [description] * @return {[type]} [description] */ remoteRemove(index) { debug('Remote remove : ', index) let removedIndex = index - 1 if (removedIndex !== -1) { this.viewEditor.deleteText(removedIndex, 1, 'silent') this.updateCommentsLinks() } } getDelta(start, end) { return this.viewEditor.editor.delta.slice(start, end) } updateCommentsLinks() { clearTimeout(this._timeout) this._timeout = setTimeout(() => { this.convertLocalLinks() this._comments.UpdateComments() }, 500) } convertLocalLinks() { $(`#${this._document._view._editorContainerID} a`) .toArray() .filter( link => link.href.includes('#/document/') || link.href.includes('#') || link.className === 'CrateSessionID' ) .forEach(link => { this.updateLink(link) }) } updateLink(l) { this.startSilence() const link = $(l) const documentId = link.attr('id') || link.attr('href').split('?')[1] link.attr('href', '#') link.attr('id', documentId) link.attr('class', 'CrateSessionID') link.attr('data-toggle', 'Open this document') link.addClass('CrateSessionID') link.click(() => { this._document.createNewDocument(documentId) }) this.stopSilence() } getAllLinksToCrate() { const linksToCrate = [] const links = $(`#${this._document._view._editorContainerID} a`) for (let l of links) { const link = $(l) if ( l.href.includes(window.location.href.split('#')[0] + '#/document/') || l.className.includes('CrateSessionID') ) { linksToCrate.push(link) } } return linksToCrate } getOperations(changesDelta) { const operations = changesDelta.ops.map(op => this.extractOperationInformation(op) ) return operations } extractOperationInformation(op) { const operation = Object.keys(op) let Name = '' let Attributes = '' let Value = '' let Length = 1 // extract attributes from the operation in the case of there existance for (let i = operation.length - 1; i >= 0; i--) { const v = op[operation[i]] if (operation[i] === 'attributes') { Attributes = v } else { Name = operation[i] Value = v } } const Type = this.getTypeOfContent(Value) if (Name === 'delete') { Length = Value } else if (Type === 'text') { Length = Value.length } debug('extractOperationInformation', { Name, Value, Attributes, Type, Length }) return { Name, Value, Attributes, Type, Length } } getTypeOfContent(value) { if (value.formula != undefined) return 'formula' if (value.video != undefined) return 'video' if (value.image != undefined) return 'image' return 'text' } }