523 lines
14 KiB
CoffeeScript
523 lines
14 KiB
CoffeeScript
structured_types_uninitialized = require "./StructuredTypes"
|
|
|
|
module.exports = (HB)->
|
|
structured_types = structured_types_uninitialized HB
|
|
types = structured_types.types
|
|
parser = structured_types.parser
|
|
|
|
#
|
|
# @nodoc
|
|
# Extends the basic Insert type to an operation that holds a text value
|
|
#
|
|
class types.TextInsert extends types.Insert
|
|
#
|
|
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
|
|
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
|
#
|
|
constructor: (content, uid, prev, next, origin, parent)->
|
|
if content?.creator
|
|
@saveOperation 'content', content
|
|
else
|
|
@content = content
|
|
super uid, prev, next, origin, parent
|
|
|
|
type: "TextInsert"
|
|
|
|
#
|
|
# Retrieve the effective length of the $content of this operation.
|
|
#
|
|
getLength: ()->
|
|
if @isDeleted()
|
|
0
|
|
else
|
|
@content.length
|
|
|
|
applyDelete: ()->
|
|
super # no braces indeed!
|
|
if @content instanceof types.Operation
|
|
@content.applyDelete()
|
|
@content = null
|
|
|
|
execute: ()->
|
|
if not @validateSavedOperations()
|
|
return false
|
|
else
|
|
if @content instanceof types.Operation
|
|
@content.insert_parent = @
|
|
super()
|
|
|
|
#
|
|
# The result will be concatenated with the results from the other insert operations
|
|
# in order to retrieve the content of the engine.
|
|
# @see HistoryBuffer.toExecutedArray
|
|
#
|
|
val: (current_position)->
|
|
if @isDeleted() or not @content?
|
|
""
|
|
else
|
|
@content
|
|
|
|
#
|
|
# Convert all relevant information of this operation to the json-format.
|
|
# This result can be send to other clients.
|
|
#
|
|
_encode: ()->
|
|
json =
|
|
{
|
|
'type': @type
|
|
'uid' : @getUid()
|
|
'prev': @prev_cl.getUid()
|
|
'next': @next_cl.getUid()
|
|
'origin': @origin.getUid()
|
|
'parent': @parent.getUid()
|
|
}
|
|
|
|
if @content?.getUid?
|
|
json['content'] = @content.getUid()
|
|
else
|
|
json['content'] = @content
|
|
json
|
|
|
|
types.TextInsert.parse = (json)->
|
|
{
|
|
'content' : content
|
|
'uid' : uid
|
|
'prev': prev
|
|
'next': next
|
|
'origin' : origin
|
|
'parent' : parent
|
|
} = json
|
|
new types.TextInsert content, uid, prev, next, origin, parent
|
|
|
|
|
|
class types.Array extends types.ListManager
|
|
|
|
type: "Array"
|
|
|
|
applyDelete: ()->
|
|
o = @end
|
|
while o?
|
|
o.applyDelete()
|
|
o = o.prev_cl
|
|
super()
|
|
|
|
cleanup: ()->
|
|
super()
|
|
|
|
toJson: (transform_to_value = false)->
|
|
val = @val()
|
|
for i, o in val
|
|
if o instanceof types.Object
|
|
o.toJson(transform_to_value)
|
|
else if o instanceof types.Array
|
|
o.toJson(transform_to_value)
|
|
else if transform_to_value and o instanceof types.Operation
|
|
o.val()
|
|
else
|
|
o
|
|
|
|
val: (pos)->
|
|
if pos?
|
|
o = @getOperationByPosition(pos+1)
|
|
if not (o instanceof types.Delimiter)
|
|
o.val()
|
|
else
|
|
throw new Error "this position does not exist"
|
|
else
|
|
o = @beginning.next_cl
|
|
result = []
|
|
while o isnt @end
|
|
if not o.isDeleted()
|
|
result.push o.val()
|
|
o = o.next_cl
|
|
result
|
|
|
|
push: (content)->
|
|
@insertAfter @end.prev_cl, content
|
|
|
|
insertAfter: (left, content, options)->
|
|
createContent = (content, options)->
|
|
if content? and content.constructor?
|
|
type = types[content.constructor.name]
|
|
if type? and type.create?
|
|
type.create content, options
|
|
else
|
|
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Yatta."
|
|
else
|
|
content
|
|
|
|
right = left.next_cl
|
|
while right.isDeleted()
|
|
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
|
|
left = right.prev_cl
|
|
|
|
if content instanceof types.Operation
|
|
(new types.TextInsert content, undefined, left, right).execute()
|
|
else
|
|
for c in content
|
|
tmp = (new types.TextInsert createContent(c, options), undefined, left, right).execute()
|
|
left = tmp
|
|
@
|
|
|
|
#
|
|
# Inserts a string into the word.
|
|
#
|
|
# @return {Array Type} This String object.
|
|
#
|
|
insert: (position, content, options)->
|
|
ith = @getOperationByPosition position
|
|
# the (i-1)th character. e.g. "abc" the 1th character is "a"
|
|
# the 0th character is the left Delimiter
|
|
@insertAfter ith, [content], options
|
|
|
|
#
|
|
# Deletes a part of the word.
|
|
#
|
|
# @return {Array Type} This String object
|
|
#
|
|
delete: (position, length)->
|
|
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
|
|
|
|
delete_ops = []
|
|
for i in [0...length]
|
|
if o instanceof types.Delimiter
|
|
break
|
|
d = (new types.Delete undefined, o).execute()
|
|
o = o.next_cl
|
|
while (not (o instanceof types.Delimiter)) and o.isDeleted()
|
|
o = o.next_cl
|
|
delete_ops.push d._encode()
|
|
@
|
|
|
|
#
|
|
# @private
|
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
|
#
|
|
_encode: ()->
|
|
json = {
|
|
'type': @type
|
|
'uid' : @getUid()
|
|
}
|
|
json
|
|
|
|
types.Array.parse = (json)->
|
|
{
|
|
'uid' : uid
|
|
} = json
|
|
new this(uid)
|
|
|
|
types.Array.create = (content, mutable)->
|
|
if (mutable is "mutable")
|
|
list = new types.Array().execute()
|
|
ith = list.getOperationByPosition 0
|
|
list.insertAfter ith, content
|
|
list
|
|
else if (not mutable?) or (mutable is "immutable")
|
|
content
|
|
else
|
|
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
|
|
|
|
#
|
|
# Handles a String-like data structures with support for insert/delete at a word-position.
|
|
# @note Currently, only Text is supported!
|
|
#
|
|
class types.String extends types.Array
|
|
|
|
#
|
|
# @private
|
|
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
|
#
|
|
constructor: (uid)->
|
|
@textfields = []
|
|
super uid
|
|
|
|
#
|
|
# Identifies this class.
|
|
# Use it to check whether this is a word-type or something else.
|
|
#
|
|
# @example
|
|
# var x = yatta.val('unknown')
|
|
# if (x.type === "String") {
|
|
# console.log JSON.stringify(x.toJson())
|
|
# }
|
|
#
|
|
type: "String"
|
|
|
|
#
|
|
# Get the String-representation of this word.
|
|
# @return {String} The String-representation of this object.
|
|
#
|
|
val: ()->
|
|
c = for o in @toArray()
|
|
if o.val?
|
|
o.val()
|
|
else
|
|
""
|
|
c.join('')
|
|
|
|
#
|
|
# Same as String.val
|
|
# @see String.val
|
|
#
|
|
toString: ()->
|
|
@val()
|
|
|
|
#
|
|
# Inserts a string into the word.
|
|
#
|
|
# @return {Array Type} This String object.
|
|
#
|
|
insert: (position, content, options)->
|
|
ith = @getOperationByPosition position
|
|
# the (i-1)th character. e.g. "abc" the 1th character is "a"
|
|
# the 0th character is the left Delimiter
|
|
@insertAfter ith, content, options
|
|
|
|
#
|
|
# Bind this String to a textfield or input field.
|
|
#
|
|
# @example
|
|
# var textbox = document.getElementById("textfield");
|
|
# yatta.bind(textbox);
|
|
#
|
|
bind: (textfield, dom_root)->
|
|
dom_root ?= window
|
|
if (not dom_root.getSelection?)
|
|
dom_root = window
|
|
|
|
# don't duplicate!
|
|
for t in @textfields
|
|
if t is textfield
|
|
return
|
|
creator_token = false;
|
|
|
|
word = @
|
|
textfield.value = @val()
|
|
@textfields.push textfield
|
|
|
|
if textfield.selectionStart? and textfield.setSelectionRange?
|
|
createRange = (fix)->
|
|
left = textfield.selectionStart
|
|
right = textfield.selectionEnd
|
|
if fix?
|
|
left = fix left
|
|
right = fix right
|
|
{
|
|
left: left
|
|
right: right
|
|
}
|
|
|
|
writeRange = (range)->
|
|
writeContent word.val()
|
|
textfield.setSelectionRange range.left, range.right
|
|
|
|
writeContent = (content)->
|
|
textfield.value = content
|
|
else
|
|
createRange = (fix)->
|
|
s = dom_root.getSelection()
|
|
clength = textfield.textContent.length
|
|
left = Math.min s.anchorOffset, clength
|
|
right = Math.min s.focusOffset, clength
|
|
if fix?
|
|
left = fix left
|
|
right = fix right
|
|
{
|
|
left: left
|
|
right: right
|
|
isReal: true
|
|
}
|
|
|
|
writeRange = (range)->
|
|
writeContent word.val()
|
|
textnode = textfield.childNodes[0]
|
|
if range.isReal and textnode?
|
|
if range.left < 0
|
|
range.left = 0
|
|
range.right = Math.max range.left, range.right
|
|
if range.right > textnode.length
|
|
range.right = textnode.length
|
|
range.left = Math.min range.left, range.right
|
|
r = document.createRange()
|
|
r.setStart(textnode, range.left)
|
|
r.setEnd(textnode, range.right)
|
|
s = window.getSelection()
|
|
s.removeAllRanges()
|
|
s.addRange(r)
|
|
writeContent = (content)->
|
|
append = ""
|
|
if content[content.length - 1] is " "
|
|
content = content.slice(0,content.length-1)
|
|
append = ' '
|
|
textfield.textContent = content
|
|
textfield.innerHTML += append
|
|
|
|
writeContent this.val()
|
|
|
|
@observe (events)->
|
|
for event in events
|
|
if not creator_token
|
|
if event.type is "insert"
|
|
o_pos = event.position
|
|
fix = (cursor)->
|
|
if cursor <= o_pos
|
|
cursor
|
|
else
|
|
cursor += 1
|
|
cursor
|
|
r = createRange fix
|
|
writeRange r
|
|
|
|
else if event.type is "delete"
|
|
o_pos = event.position
|
|
fix = (cursor)->
|
|
if cursor < o_pos
|
|
cursor
|
|
else
|
|
cursor -= 1
|
|
cursor
|
|
r = createRange fix
|
|
writeRange r
|
|
|
|
# consume all text-insert changes.
|
|
textfield.onkeypress = (event)->
|
|
if word.is_deleted
|
|
# if word is deleted, do not do anything ever again
|
|
textfield.onkeypress = null
|
|
return true
|
|
creator_token = true
|
|
char = null
|
|
if event.key?
|
|
if event.charCode is 32
|
|
char = " "
|
|
else if event.keyCode is 13
|
|
char = '\n'
|
|
else
|
|
char = event.key
|
|
else
|
|
char = window.String.fromCharCode event.keyCode
|
|
if char.length > 1
|
|
return true
|
|
else if char.length > 0
|
|
r = createRange()
|
|
pos = Math.min r.left, r.right
|
|
diff = Math.abs(r.right - r.left)
|
|
word.delete pos, diff
|
|
word.insert pos, char
|
|
r.left = pos + char.length
|
|
r.right = r.left
|
|
writeRange r
|
|
|
|
event.preventDefault()
|
|
creator_token = false
|
|
false
|
|
|
|
textfield.onpaste = (event)->
|
|
if word.is_deleted
|
|
# if word is deleted, do not do anything ever again
|
|
textfield.onpaste = null
|
|
return true
|
|
event.preventDefault()
|
|
textfield.oncut = (event)->
|
|
if word.is_deleted
|
|
# if word is deleted, do not do anything ever again
|
|
textfield.oncut = null
|
|
return true
|
|
event.preventDefault()
|
|
|
|
#
|
|
# consume deletes. Note that
|
|
# chrome: won't consume deletions on keypress event.
|
|
# keyCode is deprecated. BUT: I don't see another way.
|
|
# since event.key is not implemented in the current version of chrome.
|
|
# Every browser supports keyCode. Let's stick with it for now..
|
|
#
|
|
textfield.onkeydown = (event)->
|
|
creator_token = true
|
|
if word.is_deleted
|
|
# if word is deleted, do not do anything ever again
|
|
textfield.onkeydown = null
|
|
return true
|
|
r = createRange()
|
|
pos = Math.min(r.left, r.right, word.val().length)
|
|
diff = Math.abs(r.left - r.right)
|
|
if event.keyCode? and event.keyCode is 8 # Backspace
|
|
if diff > 0
|
|
word.delete pos, diff
|
|
r.left = pos
|
|
r.right = pos
|
|
writeRange r
|
|
else
|
|
if event.ctrlKey? and event.ctrlKey
|
|
val = word.val()
|
|
new_pos = pos
|
|
del_length = 0
|
|
if pos > 0
|
|
new_pos--
|
|
del_length++
|
|
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
|
|
new_pos--
|
|
del_length++
|
|
word.delete new_pos, (pos-new_pos)
|
|
r.left = new_pos
|
|
r.right = new_pos
|
|
writeRange r
|
|
else
|
|
if pos > 0
|
|
word.delete (pos-1), 1
|
|
r.left = pos-1
|
|
r.right = pos-1
|
|
writeRange r
|
|
event.preventDefault()
|
|
creator_token = false
|
|
return false
|
|
else if event.keyCode? and event.keyCode is 46 # Delete
|
|
if diff > 0
|
|
word.delete pos, diff
|
|
r.left = pos
|
|
r.right = pos
|
|
writeRange r
|
|
else
|
|
word.delete pos, 1
|
|
r.left = pos
|
|
r.right = pos
|
|
writeRange r
|
|
event.preventDefault()
|
|
creator_token = false
|
|
return false
|
|
else
|
|
creator_token = false
|
|
true
|
|
|
|
#
|
|
# @private
|
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
|
#
|
|
_encode: ()->
|
|
json = {
|
|
'type': @type
|
|
'uid' : @getUid()
|
|
}
|
|
json
|
|
|
|
types.String.parse = (json)->
|
|
{
|
|
'uid' : uid
|
|
} = json
|
|
new this(uid)
|
|
|
|
types.String.create = (content, mutable)->
|
|
if (mutable is "mutable")
|
|
word = new types.String().execute()
|
|
word.insert 0, content
|
|
word
|
|
else if (not mutable?) or (mutable is "immutable")
|
|
content
|
|
else
|
|
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
|
|
|
|
|
|
structured_types
|
|
|
|
|