357 lines
10 KiB
CoffeeScript
357 lines
10 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
|
|
# At the moment TextDelete type equals the Delete type in BasicTypes.
|
|
# @see BasicTypes.Delete
|
|
#
|
|
class TextDelete extends types.Delete
|
|
parser["TextDelete"] = parser["Delete"]
|
|
|
|
#
|
|
# @nodoc
|
|
# Extends the basic Insert type to an operation that holds a text value
|
|
#
|
|
class 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)->
|
|
if not (prev? and next?)
|
|
throw new Error "You must define prev, and next for TextInsert-types!"
|
|
super uid, prev, next, origin
|
|
|
|
type: "TextInsert"
|
|
|
|
#
|
|
# Retrieve the effective length of the $content of this operation.
|
|
#
|
|
getLength: ()->
|
|
if @isDeleted()
|
|
0
|
|
else
|
|
@content.length
|
|
|
|
applyDelete: ()->
|
|
@content = null
|
|
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': "TextInsert"
|
|
'content': @content
|
|
'uid' : @getUid()
|
|
'prev': @prev_cl.getUid()
|
|
'next': @next_cl.getUid()
|
|
}
|
|
if @origin isnt @prev_cl
|
|
json["origin"] = @origin.getUid()
|
|
json
|
|
|
|
parser["TextInsert"] = (json)->
|
|
{
|
|
'content' : content
|
|
'uid' : uid
|
|
'prev': prev
|
|
'next': next
|
|
'origin' : origin
|
|
} = json
|
|
new TextInsert content, uid, prev, next, origin
|
|
|
|
#
|
|
# Handles a WordType-like data structures with support for insertText/deleteText at a word-position.
|
|
# @note Currently, only Text is supported!
|
|
#
|
|
class WordType extends types.ListManager
|
|
|
|
#
|
|
# @private
|
|
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
|
#
|
|
constructor: (uid, beginning, end, prev, next, origin)->
|
|
super uid, beginning, end, prev, next, origin
|
|
|
|
#
|
|
# 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 === "WordType") {
|
|
# console.log JSON.stringify(x.toJson())
|
|
# }
|
|
#
|
|
type: "WordType"
|
|
|
|
applyDelete: ()->
|
|
o = @beginning
|
|
while o?
|
|
o.applyDelete()
|
|
o = o.next_cl
|
|
super()
|
|
|
|
cleanup: ()->
|
|
super()
|
|
|
|
push: (content)->
|
|
@insertAfter @end.prev_cl, content
|
|
|
|
insertAfter: (left, content)->
|
|
while left.isDeleted()
|
|
left = left.prev_cl # find the first character to the left, that is not deleted. Case position is 0, its the Delimiter.
|
|
right = left.next_cl
|
|
if content.type?
|
|
op = new TextInsert content, undefined, left, right
|
|
HB.addOperation(op).execute()
|
|
else
|
|
for c in content
|
|
op = new TextInsert c, undefined, left, right
|
|
HB.addOperation(op).execute()
|
|
left = op
|
|
@
|
|
#
|
|
# Inserts a string into the word.
|
|
#
|
|
# @return {WordType} This WordType object.
|
|
#
|
|
insertText: (position, content)->
|
|
# TODO: getOperationByPosition should return "(i-2)th" character
|
|
ith = @getOperationByPosition position # the (i-1)th character. e.g. "abc" a is the 0th character
|
|
left = ith.prev_cl # left is the non-deleted charather to the left of ith
|
|
@insertAfter left, content
|
|
|
|
#
|
|
# Deletes a part of the word.
|
|
#
|
|
# @return {WordType} This WordType object
|
|
#
|
|
deleteText: (position, length)->
|
|
o = @getOperationByPosition position
|
|
|
|
delete_ops = []
|
|
for i in [0...length]
|
|
if o instanceof types.Delimiter
|
|
break
|
|
d = HB.addOperation(new TextDelete 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()
|
|
@
|
|
|
|
#
|
|
# Replace the content of this word with another one. Concurrent replacements are not merged!
|
|
# Only one of the replacements will be used.
|
|
#
|
|
# @return {WordType} Returns the new WordType object.
|
|
#
|
|
replaceText: (text)->
|
|
# Can only be used if the ReplaceManager was set!
|
|
# @see WordType.setReplaceManager
|
|
if @replace_manager?
|
|
word = HB.addOperation(new WordType undefined).execute()
|
|
word.insertText 0, text
|
|
@replace_manager.replace(word)
|
|
word
|
|
else
|
|
throw new Error "This type is currently not maintained by a ReplaceManager!"
|
|
|
|
#
|
|
# 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 WordType.val
|
|
# @see WordType.val
|
|
#
|
|
toString: ()->
|
|
@val()
|
|
|
|
#
|
|
# @private
|
|
# In most cases you would embed a WordType in a Replaceable, wich is handled by the ReplaceManager in order
|
|
# to provide replace functionality.
|
|
#
|
|
setReplaceManager: (op)->
|
|
@saveOperation 'replace_manager', op
|
|
@validateSavedOperations()
|
|
@on 'insert', (event, ins)=>
|
|
@replace_manager?.forwardEvent @, 'change', ins
|
|
@on 'delete', (event, ins, del)=>
|
|
@replace_manager?.forwardEvent @, 'change', del
|
|
#
|
|
# Bind this WordType to a textfield or input field.
|
|
#
|
|
# @example
|
|
# var textbox = document.getElementById("textfield");
|
|
# yatta.bind(textbox);
|
|
#
|
|
bind: (textfield)->
|
|
word = @
|
|
textfield.value = @val()
|
|
|
|
@on "insert", (event, op)->
|
|
o_pos = op.getPosition()
|
|
fix = (cursor)->
|
|
if cursor <= o_pos
|
|
cursor
|
|
else
|
|
cursor += 1
|
|
cursor
|
|
left = fix textfield.selectionStart
|
|
right = fix textfield.selectionEnd
|
|
|
|
textfield.value = word.val()
|
|
textfield.setSelectionRange left, right
|
|
|
|
|
|
@on "delete", (event, op)->
|
|
o_pos = op.getPosition()
|
|
fix = (cursor)->
|
|
if cursor < o_pos
|
|
cursor
|
|
else
|
|
cursor -= 1
|
|
cursor
|
|
left = fix textfield.selectionStart
|
|
right = fix textfield.selectionEnd
|
|
|
|
textfield.value = word.val()
|
|
textfield.setSelectionRange left, right
|
|
|
|
# consume all text-insert changes.
|
|
textfield.onkeypress = (event)->
|
|
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 = String.fromCharCode event.keyCode
|
|
if char.length > 0
|
|
pos = Math.min textfield.selectionStart, textfield.selectionEnd
|
|
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
|
|
word.deleteText (pos), diff
|
|
word.insertText pos, char
|
|
new_pos = pos + char.length
|
|
textfield.setSelectionRange new_pos, new_pos
|
|
event.preventDefault()
|
|
else
|
|
event.preventDefault()
|
|
|
|
textfield.onpaste = (event)->
|
|
event.preventDefault()
|
|
textfield.oncut = (event)->
|
|
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)->
|
|
pos = Math.min textfield.selectionStart, textfield.selectionEnd
|
|
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
|
|
if event.keyCode? and event.keyCode is 8 # Backspace
|
|
if diff > 0
|
|
word.deleteText pos, diff
|
|
textfield.setSelectionRange pos, pos
|
|
else
|
|
if event.ctrlKey? and event.ctrlKey
|
|
val = textfield.value
|
|
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.deleteText new_pos, (pos-new_pos)
|
|
textfield.setSelectionRange new_pos, new_pos
|
|
else
|
|
word.deleteText (pos-1), 1
|
|
event.preventDefault()
|
|
else if event.keyCode? and event.keyCode is 46 # Delete
|
|
if diff > 0
|
|
word.deleteText pos, diff
|
|
textfield.setSelectionRange pos, pos
|
|
else
|
|
word.deleteText pos, 1
|
|
textfield.setSelectionRange pos, pos
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
#
|
|
# @private
|
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
|
#
|
|
_encode: ()->
|
|
json = {
|
|
'type': "WordType"
|
|
'uid' : @getUid()
|
|
'beginning' : @beginning.getUid()
|
|
'end' : @end.getUid()
|
|
}
|
|
if @prev_cl?
|
|
json['prev'] = @prev_cl.getUid()
|
|
if @next_cl?
|
|
json['next'] = @next_cl.getUid()
|
|
if @origin? # and @origin isnt @prev_cl
|
|
json["origin"] = @origin().getUid()
|
|
json
|
|
|
|
parser['WordType'] = (json)->
|
|
{
|
|
'uid' : uid
|
|
'beginning' : beginning
|
|
'end' : end
|
|
'prev': prev
|
|
'next': next
|
|
'origin' : origin
|
|
} = json
|
|
new WordType uid, beginning, end, prev, next, origin
|
|
|
|
types['TextInsert'] = TextInsert
|
|
types['TextDelete'] = TextDelete
|
|
types['WordType'] = WordType
|
|
structured_types
|
|
|
|
|