added text as a custom type, more tests are working
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 += ' '
|
||||
|
||||
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
0
lib/Types/List.coffee
Normal 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
306
lib/Types/Text.coffee
Normal 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 += ' '
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user