added text as a custom type, more tests are working

This commit is contained in:
DadaMonad
2015-02-23 11:41:04 +00:00
parent 860934de06
commit 2e9f8f6d03
22 changed files with 2604 additions and 2255 deletions

View File

@@ -22,7 +22,9 @@ module.exports = ()->
# @param {Object} uid A unique identifier.
# If uid is undefined, a new uid will be created before at the end of the execution sequence
#
constructor: (uid)->
constructor: (custom_type, uid)->
if custom_type?
@custom_type = custom_type
@is_deleted = false
@garbage_collected = false
@event_listeners = [] # TODO: rename to observers or sth like that
@@ -221,9 +223,9 @@ module.exports = ()->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} deletes UID or reference of the operation that this to be deleted.
#
constructor: (uid, deletes)->
constructor: (custom_type, uid, deletes)->
@saveOperation 'deletes', deletes
super uid
super custom_type, uid
type: "Delete"
@@ -260,7 +262,7 @@ module.exports = ()->
'uid' : uid
'deletes': deletes_uid
} = o
new this(uid, deletes_uid)
new this(null, uid, deletes_uid)
#
# @nodoc
@@ -279,7 +281,7 @@ module.exports = ()->
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
#
constructor: (content, uid, prev_cl, next_cl, origin, parent)->
constructor: (custom_type, content, uid, prev_cl, next_cl, origin, parent)->
# see encode to see, why we are doing it this way
if content is undefined
# nop
@@ -294,7 +296,7 @@ module.exports = ()->
@saveOperation 'origin', origin
else
@saveOperation 'origin', prev_cl
super uid
super custom_type, uid
type: "Insert"
@@ -436,19 +438,24 @@ module.exports = ()->
@
callOperationSpecificInsertEvents: ()->
getContentType = (content)->
if content instanceof ops.Operation
content.getCustomType()
else
content
@parent?.callEvent [
type: "insert"
position: @getPosition()
object: @parent
object: @parent.getCustomType()
changedBy: @uid.creator
value: @content
value: getContentType @content
]
callOperationSpecificDeleteEvents: (o)->
@parent.callEvent [
type: "delete"
position: @getPosition()
object: @parent # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
object: @parent.getCustomType() # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
length: 1
changedBy: o.uid.creator
]
@@ -503,7 +510,7 @@ module.exports = ()->
} = json
if typeof content is "string"
content = JSON.parse(content)
new this content, uid, prev, next, origin, parent
new this null, content, uid, prev, next, origin, parent
@@ -517,8 +524,8 @@ module.exports = ()->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, @content)->
super uid
constructor: (custom_type, uid, @content)->
super custom_type, uid
type: "ImmutableObject"
@@ -544,7 +551,7 @@ module.exports = ()->
'uid' : uid
'content' : content
} = json
new this(uid, content)
new this(null, uid, content)
#
# @nodoc
@@ -562,7 +569,7 @@ module.exports = ()->
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
@saveOperation 'origin', prev_cl
super {noOperation: true}
super null, {noOperation: true}
type: "Delimiter"

View File

@@ -14,10 +14,8 @@ module.exports = ()->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (custom_type, uid)->
if custom_type?
@custom_type = custom_type
@_map = {}
super uid
super custom_type, uid
type: "MapManager"
@@ -39,8 +37,8 @@ module.exports = ()->
#
val: (name, content)->
if arguments.length > 1
if content? and content._model? and content._model instanceof ops.Operation
rep = content._model
if content? and content._getModel?
rep = content._getModel(@custom_types, @operations)
else
rep = content
@retrieveSub(name).replace rep
@@ -59,11 +57,7 @@ module.exports = ()->
result = {}
for name,o of @_map
if not o.isContentDeleted()
res = prop.val()
if res instanceof ops.Operation
result[name] = res.getCustomType()
else
result[name] = res
result[name] = o.val()
result
delete: (name)->
@@ -79,7 +73,7 @@ module.exports = ()->
noOperation: true
sub: property_name
alt: @
rm = new ops.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
rm = new ops.ReplaceManager null, event_properties, event_this, rm_uid # this operation shall not be saved in the HB
@_map[property_name] = rm
rm.setParent @, property_name
rm.execute()
@@ -119,13 +113,13 @@ module.exports = ()->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (uid)->
constructor: (custom_type, uid)->
@beginning = new ops.Delimiter undefined, undefined
@end = new ops.Delimiter @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super uid
super custom_type, uid
type: "ListManager"
@@ -238,27 +232,18 @@ module.exports = ()->
push: (content)->
@insertAfter @end.prev_cl, content
insertAfter: (left, content, options)->
createContent = (content, options)->
if content? and content.constructor?
type = ops[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 Y."
else
content
insertAfter: (left, 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
# TODO: always expect an array as content. Then you can combine this with the other option (else)
if content instanceof ops.Operation
(new ops.Insert content, undefined, left, right).execute()
(new ops.Insert null, content, undefined, left, right).execute()
else
for c in content
tmp = (new ops.Insert createContent(c, options), undefined, left, right).execute()
tmp = (new ops.Insert null, c, undefined, left, right).execute()
left = tmp
@
@@ -267,11 +252,11 @@ module.exports = ()->
#
# @return {ListManager Type} This String object.
#
insert: (position, content, options)->
insert: (position, content)->
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
@insertAfter ith, [content]
#
# Deletes a part of the word.
@@ -285,7 +270,7 @@ module.exports = ()->
for i in [0...length]
if o instanceof ops.Delimiter
break
d = (new ops.Delete undefined, o).execute()
d = (new ops.Delete null, undefined, o).execute()
o = o.next_cl
while (not (o instanceof ops.Delimiter)) and o.isDeleted()
o = o.next_cl
@@ -301,26 +286,18 @@ module.exports = ()->
'type': @type
'uid' : @getUid()
}
if @custom_type.constructor is String
json.custom_type = @custom_type
else
json.custom_type = @custom_type._name
json
ops.ListManager.parse = (json)->
{
'uid' : uid
'custom_type': custom_type
} = json
new this(uid)
ops.Array = ()->
ops.Array.create = (content, mutable)->
if (mutable is "mutable")
list = new ops.ListManager().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\"!!"
new this(custom_type, uid)
#
# @nodoc
@@ -338,10 +315,10 @@ module.exports = ()->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (@event_properties, @event_this, uid, beginning, end)->
constructor: (custom_type, @event_properties, @event_this, uid, beginning, end)->
if not @event_properties['object']?
@event_properties['object'] = @event_this
super uid, beginning, end
super custom_type, uid, beginning, end
type: "ReplaceManager"
@@ -378,7 +355,7 @@ module.exports = ()->
#
replace: (content, replaceable_uid)->
o = @getLastOperation()
relp = (new ops.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
relp = (new ops.Replaceable null, content, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging)
undefined
@@ -386,7 +363,7 @@ module.exports = ()->
@getLastOperation().isDeleted()
deleteContent: ()->
(new ops.Delete undefined, @getLastOperation().uid).execute()
(new ops.Delete null, undefined, @getLastOperation().uid).execute()
undefined
#
@@ -424,9 +401,9 @@ module.exports = ()->
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, parent, uid, prev, next, origin, is_deleted)->
constructor: (custom_type, content, parent, uid, prev, next, origin, is_deleted)->
@saveOperation 'parent', parent
super content, uid, prev, next, origin # Parent is already saved by Replaceable
super custom_type, content, uid, prev, next, origin # Parent is already saved by Replaceable
@is_deleted = is_deleted
type: "Replaceable"
@@ -525,8 +502,9 @@ module.exports = ()->
'next': next
'origin' : origin
'is_deleted': is_deleted
'custom_type' : custom_type
} = json
new this(content, parent, uid, prev, next, origin, is_deleted)
new this(custom_type, content, parent, uid, prev, next, origin, is_deleted)
basic_ops

View File

@@ -1,308 +0,0 @@
structured_ops_uninitialized = require "./Structured"
module.exports = ()->
structured_ops = structured_ops_uninitialized()
ops = structured_ops.operations
#
# Handles a String-like data structures with support for insert/delete at a word-position.
# @note Currently, only Text is supported!
#
class ops.String extends ops.ListManager
#
# @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 = y.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: ()->
@fold "", (left, o)->
left + o.val()
#
# Same as String.val
# @see String.val
#
toString: ()->
@val()
#
# Inserts a string into the word.
#
# @return {ListManager 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");
# y.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)->
range = {}
s = dom_root.getSelection()
clength = textfield.textContent.length
range.left = Math.min s.anchorOffset, clength
range.right = Math.min s.focusOffset, clength
if fix?
range.left = fix range.left
range.right = fix range.right
edited_element = s.focusNode
if edited_element is textfield or edited_element is textfield.childNodes[0]
range.isReal = true
else
range.isReal = false
range
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)->
content_array = content.replace(new RegExp("\n",'g')," ").split(" ")
textfield.innerText = ""
for c, i in content_array
textfield.innerText += c
if i isnt content_array.length-1
textfield.innerHTML += '&nbsp;'
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.keyCode is 13
char = '\n'
else if event.key?
if event.charCode is 32
char = " "
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
ops.String.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
ops.String.create = (content, mutable)->
if (mutable is "mutable")
word = new ops.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_ops

0
lib/Types/List.coffee Normal file
View File

View File

@@ -55,6 +55,9 @@ class YObject
res[n] = v
res
delete: (name)->
@_model.delete(name)
if window?
if window.Y?
window.Y.Object = YObject

306
lib/Types/Text.coffee Normal file
View File

@@ -0,0 +1,306 @@
#
# Handles a String-like data structures with support for insert/delete at a word-position.
# @note Currently, only Text is supported!
#
class YText
#
# @private
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (text)->
@textfields = []
if not text?
@_text = ""
else if text.constructor is String
@_text = text
else
throw new Error "Y.Text expects a String as a constructor"
_name: "Text"
_getModel: (types, ops)->
if not @_model?
@_model = new ops.ListManager(@).execute()
@insert 0, @_text
delete @_text
@_model
_setModel: (@_model)->
delete @_text
#
# Get the String-representation of this word.
# @return {String} The String-representation of this object.
#
val: ()->
@_model.fold "", (left, o)->
left + o.val()
observe: ()->
@_model.observe.apply @_model, arguments
unobserve: ()->
@_model.unobserve.apply @_model, arguments
#
# Same as String.val
# @see String.val
#
toString: ()->
@val()
#
# Inserts a string into the word.
#
# @return {ListManager Type} This String object.
#
insert: (position, content)->
if content.constructor isnt String
throw new Error "Y.String.insert expects a String as the second parameter!"
if typeof position isnt "number"
throw new Error "Y.String.insert expects a Number as the second parameter!"
if content.length > 0
ith = @_model.getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@_model.insertAfter ith, content
delete: (position, length)->
@_model.delete position, length
#
# Bind this String to a textfield or input field.
#
# @example
# var textbox = document.getElementById("textfield");
# y.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)->
range = {}
s = dom_root.getSelection()
clength = textfield.textContent.length
range.left = Math.min s.anchorOffset, clength
range.right = Math.min s.focusOffset, clength
if fix?
range.left = fix range.left
range.right = fix range.right
edited_element = s.focusNode
if edited_element is textfield or edited_element is textfield.childNodes[0]
range.isReal = true
else
range.isReal = false
range
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)->
content_array = content.replace(new RegExp("\n",'g')," ").split(" ")
textfield.innerText = ""
for c, i in content_array
textfield.innerText += c
if i isnt content_array.length-1
textfield.innerHTML += '&nbsp;'
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.keyCode is 13
char = '\n'
else if event.key?
if event.charCode is 32
char = " "
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
if window?
if window.Y?
window.Y.Text = YText
else
throw new Error "You must first import Y!"
if module?
module.exports = YText

View File

@@ -1,5 +1,5 @@
text_ops_uninitialized = require "./Operations/Text"
structured_ops_uninitialized = require "./Operations/Structured"
HistoryBuffer = require "./HistoryBuffer"
Engine = require "./Engine"
@@ -15,7 +15,7 @@ createY = (connector)->
user_id = id
HB.resetUserId id
HB = new HistoryBuffer user_id
ops_manager = text_ops_uninitialized HB, this.constructor
ops_manager = structured_ops_uninitialized HB, this.constructor
ops = ops_manager.operations
engine = new Engine HB, ops
@@ -25,7 +25,7 @@ createY = (connector)->
ops.Operation.prototype.operations = ops
ops.Operation.prototype.engine = engine
ops.Operation.prototype.connector = connector
ops.Operation.prototype.custom_ops = this.constructor
ops.Operation.prototype.custom_types = this.constructor
ct = new createY.Object()
model = new ops.MapManager(ct, HB.getReservedUniqueIdentifier()).execute()
@@ -37,3 +37,4 @@ if window?
window.Y = createY
createY.Object = require "./Types/Object"
createY.Text = require "./Types/Text"