lib/controller/editor.js
var Marker = require('../view/marker.js')
window.Marker = Marker
/**
* EditorController this the link between the core functions and the interface.
*/
class EditorController {
/**
* [constructor description]
* @param {[doc]} model this is the object that contains all proprieties of a document.
* @param {[string]} sessionID [description]
*/
constructor(model, sessionID) {
/**
* this is the object that contains all proprieties of a document.
* @type {[doc]}
*/
this.model = model
/**
* markers contains all marks of the users: carets, avatars...
* @type {Marker[]}
*/
this.markers ={}
/**
* startimer A timer used for sending pings
* @type {Timer}
*/
this.startTimer= {}
/**
* ViewEditor the used editor, here it is Quill editor
* @see https://quilljs.com/
* @type {Quill}
*/
this.viewEditor= {};
this.loadDocument()
let commentOpt = quill.options.modules.comment
commentOpt.commentAddOn = this.markers[id].animal
commentOpt.commentAuthorId = this.model.uid
commentOpt.color = this.markers[id].colorRGB
this.startPing(2000)
jQuery("#saveicon").click(function() {
this.saveDocument()
})
jQuery("#copyButton").click(function() {
this.copyLink()
})
jQuery('#title').focusout(function() {
this.changeTitle()
})
// EDITOR listeners
this.viewEditor.on('selection-change', (range, oldRange, source) => {
if (range) {
this.model.core.caretMoved(range)
}
})
this.viewEditor.on('text-change', (delta, oldDelta, source) => {
this.textChange(delta, oldDelta, source)
})
// Core listeners
model.core.on('remoteInsert', (element, indexp) => {
this.remoteInsert(element, indexp)
})
model.core.on('remoteRemove', (index) => {
this.remoteRemove(index)
})
model.core.on('remoteCaretMoved', (range, origin) => {
this.remoteCaretMoved(range, origin)
})
model.core.on('remoteCaretMoved', (range, origin) => {
this.remoteCaretMoved(range, origin)
})
//At the reception of Title changed operation
model.core.on('changeTitle', (title) => {
jQuery('#title').text(title)
})
model.core.on('ping', (origin, pseudo) => {
this.atPing(origin, pseudo)
})
}
/**
* loadDocument load the document if it exist in the local storage
* @return {[type]} [description]
*/
loadDocument() {
this.viewEditor = quill
Marker.cursors = this.viewEditor.getModule('cursors')
jQuery("#editor").attr('id', 'crate-' + id)
// Initilise the the editor content
//this.editor.setText('')
if (store.get("CRATE2-" + sessionID)) {
var doc = store.get("CRATE2-" + sessionID)
viewEditor.setContents(doc.delta, "user")
jQuery("#title").text(doc.title)
}
// make title editable
jQuery('#title').click(function() {
jQuery('#title').attr('contenteditable', 'true')
})
if (store.get("CRATE2-" + this.model.signalingOptions.session)) {
this.markers = store.get("CRATE2-" + this.model.signalingOptions.session).markers
// convert the json objects to Marker object with functions
for (var property in this.markers) {
if (this.markers.hasOwnProperty(property)) {
this.markers[property] = Object.assign(new Marker(property), this.markers[property])
this.markers[property].cursor = false
}
}
} else {
this.markers = {}
}
this.model.markers = this.markers
//if (!this.markers[id]) {
// console.log("Add mythis")
//this.markers.push(id)
id = this.model.uid
this.markers[id] = new Marker(id, 5 * 1000, {
index: 0,
length: 0
}, this.viewEditor.getModule('cursors'), false, true)
if (store.get('myId')) {
this.markers[id].setPseudo(store.get('myId').pseudo)
} else {
store.set('myId', {
id: id,
pseudo: this.markers[id].pseudoName
})
}
this.UpdateComments()
}
/**
* saveDocument save the document in local storage
* @return {[type]} [description]
*/
saveDocument() {
var timeNow = new Date().getTime()
var title = jQuery("#title").text()
var document = {
date: timeNow,
title: title,
delta: this.viewEditor.editor.editor.delta,
sequence: this.model.sequence,
causality: this.model.causality,
name: this.model.name,
webRTCOptions: this.model.webRTCOptions,
markers: this.markers,
signalingOptions: this.model.signalingOptions
}
store.set("CRATE2-" + this.model.signalingOptions.session, document)
alert("Document is saved successfully")
}
/**
* copyLink copy the link of the document
* @return {[type]} [description]
*/
copyLink() {
jQuery("#sessionUrl").select()
document.execCommand("Copy")
}
/**
* 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) {
this.cleanQuill()
this.applyChanges(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 changes = JSON.parse(JSON.stringify(delta, null, 2))
let retain = iniRetain
let text = ""
changes.ops.forEach((op) => {
var operation = Object.keys(op)
var oper = ""
var att = ""
var value = ""
// extract attributes from the operation in the case of there existance
for (var i = operation.length - 1; i >= 0; i--) {
var v = op[operation[i]]
if (operation[i] === "attributes") {
att = v
} else {
oper = operation[i]
value = v
}
}
// Change the style = remove the word and insert again with attribues,
// In the case of changing the style, delta will contain "retain" (the start postion) with attributes
var isItInsertWithAtt = false // to know if we have to update comments or not, if its delete because of changing style, no update comments is needed
retain = this.sendIt(text, att, 0, value, oper, retain, isItInsertWithAtt)
})
}
/**
* sendIt Send the changes character by character
* @param {[type]} text [description]
* @param {[type]} att [description]
* @param {[type]} start [description]
* @param {[type]} value [description]
* @param {[type]} oper [description]
* @param {[type]} retain [description]
* @param {Boolean} isItInsertWithAtt [description]
* @return {[type]} [description]
*/
sendIt(text, att, start, value, oper, retain, isItInsertWithAtt) {
switch (oper) {
case "retain":
if (att != "") {
let isItInsertWithAtt = true
// the value in this case is the end of format
// insert the changed text with the new attributes
// 1 delete the changed text from retain to value
this.sendIt("", "", retain, value, "delete", retain, isItInsertWithAtt)
// 2 insert with attributes
var Deltat = this.viewEditor.editor.delta.slice(retain, retain + value)
this.applyChanges(Deltat, retain)
} else {
retain += value
}
// If there is attributes than delete and then insert
break
case "insert":
text = value
// Insert character by character or by object for the other formats
// this is a formula
if (value.formula != undefined) {
att = this.viewEditor.getFormat(retain, 1)
this.model.core.insert({
type: 'formula',
text: value,
att: att
}, retain)
} else
{
// this is a video
if (value.video != undefined) {
att = this.viewEditor.getFormat(retain, 1)
this.model.core.insert({
type: 'video',
text: value,
att: att
}, retain)
} else {
// It is an image
if (value.image != undefined) {
att = this.viewEditor.getFormat(retain, 1)
this.model.core.insert({
type: 'image',
text: value,
att: att
}, retain)
} else { // text
for (var i = retain; i < (retain + text.length); ++i) {
att = this.viewEditor.getFormat(i, 1)
this.model.core.insert({
type: 'char',
text: text[i - retain],
att: att
}, i)
}
retain = retain + text.length
}
}
}
break
case "delete":
var length = value
//to ensure that the editor contains just \n without any attributes
if (!isItInsertWithAtt) {
this.UpdateComments()
}
if (start == 0) {
start = retain
}
// Delete caracter by caracter
for (var i = start; i < (start + length); ++i) {
this.model.core.remove(start)
}
break
}
return retain
}
/**
* 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
if (index !== -1) {
switch (element.type) {
case "formula":
this.viewEditor.insertEmbed(index, 'formula', element.text.formula, 'silent')
break
case "image":
this.viewEditor.insertEmbed(index, 'image', element.text.image, 'silent')
break
case "video":
this.viewEditor.insertEmbed(index, 'video', element.text.video, 'silent')
break
case "char":
this.viewEditor.insertText(index, element.text, element.att, 'silent')
if (element.text != "\n") {
this.viewEditor.removeFormat(index, 1, 'silent')
}
break
}
if (element.att) {
if (element.text != "\n") {
this.viewEditor.formatLine(index, element.att, 'silent')
this.viewEditor.formatText(index, 1, element.att, 'silent')
}
if (element.att.commentAuthor) {
this.UpdateComments()
}
}
}
this.cleanQuill()
}
/**
* remoteRemove At the reception of remove operation
* @param {[type]} index [description]
* @return {[type]} [description]
*/
remoteRemove(index) {
let removedIndex = index - 1
if (removedIndex !== -1) {
this.viewEditor.deleteText(removedIndex, 1, 'silent')
this.UpdateComments()
}
this.cleanQuill()
}
/**
* remoteCaretMoved At the reception of CARET position
* @param {[type]} range [description]
* @param {[type]} origin [description]
* @return {[type]} [description]
*/
remoteCaretMoved(range, origin) {
if (!origin) return
if (this.markers[origin]) {
this.markers[origin].update(range, true) // to keep avatar
} else { // to crevat the avatar
//this.markers.push(origin)
this.markers[origin] = new Marker(origin, 5 * 1000, range, editor.getModule('cursors'), false)
}
}
/**
* cleanQuill description
* @return {[type]} [description]
*/
cleanQuill() {
/*
delta = quill.editor.delta
console.log('before clean')
console.dir(delta)
lastOperation=delta.ops.length-1
if (delta.ops[lastOperation].insert=='\n' && lastOperation != 0) {
attributes= delta.ops[lastOperation].attributes
delete delta.ops[lastOperation]
//delta.ops.splice(length-1,1)
if(lastOperation-1 != 0 && delta.ops[lastOperation-1]) {
delta.ops[lastOperation-1].attributes=attributes
}
}
/*if (delta.ops[0].insert=='\n' && quill.getLength() <=2) {
delta.ops[0].attributes={}
}
console.log('after clean')
console.dir(delta)
//quill.setContents(delta,'silent')*/
}
/**
* changeTitle For any change in title, broadcast the new title
* @return {[type]} [description]
*/
changeTitle() {
jQuery('#title').attr('contenteditable', 'false')
if (jQuery('#title').text() == "") {
jQuery('#title').text('Untitled document')
}
model.name = jQuery('#title').text()
//TODO: Optimize change only if the text is changed from last state
model.core.sendChangeTitle(jQuery('#title').text())
}
/**
* startPing send periodically ping
* @param {[type]} interval [description]
* @return {[type]} [description]
* @todo TODO: Make interval as global parameter
*/
startPing(interval) {
this.startTimer = setInterval(() => {
this.model.core.sendPing()
}, interval)
}
/**
* stopPing stopPing
* @todo implement this function
* @return {[type]} [description]
*/
stopPing() {
}
/**
* atPing at the reception of ping
* @param {[type]} origin [description]
* @param {[type]} pseudo [description]
* @return {[type]} [description]
*/
atPing(origin, pseudo) {
if (this.markers[origin]) {
this.markers[origin].update(null, false) // to keep avatar
this.markers[origin].setPseudo(pseudo)
} else { // to create the avatar
//this.markers.push(origin)
this.markers[origin] = new Marker(origin, 5 * 1000, {
index: 0,
length: 0
}, this.viewEditor.getModule('cursors'), false, false)
this.markers[origin].setPseudo(pseudo)
}
}
/**
* UpdateComments This function to extract the comments form the editor and show them in #comments
*/
UpdateComments() {
// clear comments
jQuery("#comments").empty()
// for each insert check att if it contains the author then insert comment
quill.editor.delta.ops.forEach(function(op) {
if (op.insert) {
if (op.attributes) {
if (op.attributes.commentAuthor) {
var id = op.attributes.commentAuthor
//
if (this.markers[id]) {
m = this.markers[id]
} else {
m = new Marker(id)
}
animal = m.animal
pseudoName = m.pseudoName
colorRGB = m.colorRGB
addCommentToList(op.attributes.comment, id, animal, pseudoName, colorRGB, op.attributes.commentTimestamp)
}
}
}
})
}
}
module.exports = EditorController