ret = ret.substr 0, clip + 1
return ret
+# table too look up the properties of various values for css's white-space
+ws_props =
+ normal:
+ space: false # spaces are not preserved/rendered
+ newline: false # newlines are not preserved/rendered
+ wrap: true # text is word-wrapped
+ to_preserve: 'pre-wrap' # to preservespaces, change white-space to this
+ nowrap:
+ space: false
+ newline: false
+ wrap: false
+ to_preserve: 'pre'
+ 'pre-line':
+ space: false
+ newline: true
+ wrap: true
+ to_preserve: 'pre-wrap'
+ pre:
+ space: true
+ newline: true
+ wrap: false
+ 'pre-wrap':
+ space: true
+ newline: true
+ wrap: true
+
# xml 1.0 spec, chromium and firefox accept these, plus lots of unicode chars
valid_attr_regex = new RegExp '^[a-zA-Z_:][-a-zA-Z0-9_:.]*$'
# html5 spec is much more lax, but chromium won't let me make at attribute with the name "4"
range.setEnd el, end
rects = range.getClientRects()
if rects.length > 0
- rect = rects[0]
+ if rects.length > 1
+ if rects[1].width > rects[0].width
+ rect = rects[1]
+ else
+ rect = rects[0]
+ else
+ rect = rects[0]
else
return null
doc = el.ownerDocument.documentElement
@move_cursor new_cursor
return false
when KEY_DOWN
+ if @cursor?
+ new_cursor = @cursor
+ # go next until we move on the y axis
+ while new_cursor.y <= @cursor.y
+ new_cursor = find_next_cursor_position @tree, new_cursor
+ return false unless new_cursor?
+ # done early if we're already right of old cursor position
+ if new_cursor.x >= @cursor.x
+ # this would be strange, but could happen due to runaround
+ @move_cursor new_cursor
+ return false
+ target_y = new_cursor.y
+ # search rightward, until we find the closest position
+ # new_cursor is the next-most position we've checked
+ # prev_cursor is the older value, so it's not as next as new_cursor
+ while new_cursor.x < @cursor.x and new_cursor.y is target_y
+ prev_cursor = new_cursor
+ new_cursor = find_next_cursor_position @tree, new_cursor
+ break unless new_cursor?
+ # move cursor to prev_cursor or new_cursor
+ if new_cursor?
+ if new_cursor.y is target_y
+ # both valid, and on the same line, use closest
+ if (new_cursor.x - @cursor.x) < (@cursor.x - prev_cursor.x)
+ @move_cursor new_cursor
+ else
+ @move_cursor prev_cursor
+ else
+ # new_cursor on wrong line, use prev_cursor
+ @move_cursor prev_cursor
+ else
+ # can't go any further prev, use prev_cursor
+ @move_cursor prev_cursor
+ else
+ # move cursor to first position in document
+ new_cursor = last_cursor_position @tree
+ if new_cursor?
+ @move_cursor new_cursor
return false
when KEY_END
return false
when KEY_BACKSPACE
return false unless @cursor?
- return false unless @cursor[1] > 0
- @cursor[0].text = @cursor[0].text.substr(0, @cursor[1] - 1) + @cursor[0].text.substr(@cursor[1])
- @cursor[0].el.nodeValue = @cursor[0].text
- @move_cursor [@cursor[0], @cursor[1] - 1]
+ return false unless @cursor.i > 0
+ @cursor.n.text = @cursor.n.text.substr(0, @cursor.i - 1) + @cursor.n.text.substr(@cursor.i)
+ @cursor.n.el.nodeValue = @cursor.n.text
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor()
@changed()
return false
when KEY_DELETE
return false unless @cursor?
- return false unless @cursor[1] < @cursor[0].text.length
- @cursor[0].text = @cursor[0].text.substr(0, @cursor[1]) + @cursor[0].text.substr(@cursor[1] + 1)
- @cursor[0].el.nodeValue = @cursor[0].text
- @move_cursor [@cursor[0], @cursor[1]]
+ return false unless @cursor.i < @cursor.n.text.length
+ @cursor.n.text = @cursor.n.text.substr(0, @cursor.i) + @cursor.n.text.substr(@cursor.i + 1)
+ @cursor.n.el.nodeValue = @cursor.n.text
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor()
@changed()
return false
when KEY_ENTER
onkeypress: (e) ->
return if e.ctrlKey
return false if ignore_key_codes[e.keyCode]?
- # return false if control_key_codes[e.keyCode]? # handled in keydown
char = e.charCode ? e.keyCode
if char and @cursor?
char = String.fromCharCode char
- if @cursor[1] is 0
- @cursor[0].text = char + @cursor[0].text
- else if @cursor[1] is @cursor[0].text.length - 1
- @cursor[0].text += char
+ @insert_character @cursor.n, @cursor.i, char
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i + 1
+ if new_cursor
+ @move_cursor new_cursor
else
- @cursor[0].text =
- @cursor[0].text.substr(0, @cursor[1]) +
- char +
- @cursor[0].text.substr(@cursor[1])
- @cursor[0].el.nodeValue = @cursor[0].text
- @move_cursor [@cursor[0], @cursor[1] + 1]
+ console.log "ERROR: couldn't find cursor position after insert"
+ @kill_cursor()
@changed()
return false
clear_dom: -> # remove all the editable content (and cursor, overlays, etc)
@iframe.style.height = "0"
@iframe.style.height = "#{h}px"
@wrap2.scrollTop = s
+ # Warning: this does not call changed() for you
+ insert_character: (n, i, char) ->
+ # TODO handle newlines, tabs, etc
+ parent = @cursor.n.parent
+ return unless parent
+ return unless parent.el?
+ style = @iframe.contentWindow.getComputedStyle parent.el, null
+ ws = style.getPropertyValue 'white-space'
+ if char is ' '
+ unless ws_props[ws].space
+ change = false
+ if i is 0
+ # TODO check if a space at the beginning would actually get collapsed
+ change = true
+ else if i is n.text.length
+ change = true
+ # TODO check if a space at the end would actually get collapsed
+ else
+ if n.text.charAt(i - 1) is ' ' or n.text.charAt(i) is ' '
+ change = true
+ if change
+ rule = "white-space: #{ws_props[ws].to_preserve}"
+ if parent.attrs[style]?
+ parent.attrs.style += "; #{rule}"
+ else
+ parent.attrs.style = rule
+ parent.el.setAttribute 'style', parent.attrs.style
+ else
+ # TODO test this
+ # inserting a visible (non-space) character
+ if ws_props[ws].space
+ if parent.el.style?['white-space']
+ # This node has a "white-space" property on it
+ # probably created automatically by this editor
+ # when the user pressed space.
+ # Check if that's no longer needed.
+ need = false
+ for ti in [0...n.text.length]
+ code = n.text.charCodeAt ti
+ if code isnt 32 and is_space_code code
+ # tab, return
+ need = true
+ break
+ # check for double spaces that don't surround insert location
+ continue if ti is i
+ continue if ti is 0
+ if n.text.substr(ti - 1, 2) is ' '
+ need = true
+ break
+ if i > 0
+ if 32 is n.text.charCodeAt 0
+ need = true
+ if i < n.text.length
+ if 32 is n.text.charCodeAt n.text.length - 1
+ need = true
+ unless need
+ # TODO don't assume whitespace is just so
+ if parent.attrs.style is "white-space: #{ws}"
+ delete parent.attrs.style
+ parent.el.removeAttribute 'style'
+ else
+ # FIXME find it in the middle and at the start
+ needle = "; white-space: #{ws}"
+ if needle is parent.attrs.style.substr parent.attrs.style.length - needle
+ parent.attrs.style = parent.attrs.style.substr 0, parent.attrs.style.length - needle
+ parent.el.setAttribute parent.attrs.style
+ # TODO insert the character now
+ if i is 0
+ n.text = char + n.text
+ else if i is n.text.length - 1
+ n.text += char
+ else
+ n.text =
+ n.text.substr(0, i) +
+ char +
+ n.text.substr(i)
+ n.el.nodeValue = n.text
+ # TODO call this when the user types
+ # TODO detect when typing produces a collapsing space
+ remove_character: (n, i) ->
+ # TODO call this from delete and backspace key handlers
+ # TODO detect if this would result in collapsing space
kill_cursor: -> # remove it, forget where it was
if @cursor_visible
@cursor_el.parentNode.removeChild @cursor_el
@cursor_visible = false
@cursor = null
- @matt null
+ @annotate null
move_cursor: (cursor) ->
@cursor = cursor
unless @cursor_visible
height = cursor.h
@cursor_el.style.top = "#{cursor.y + overlay_padding + Math.round(height * .07)}px"
@cursor_el.style.height = "#{Math.round height * 0.82}px"
- @matt cursor.n
- matt: (n) ->
+ @annotate cursor.n
+ annotate: (n) ->
while @matting.length > 0
@overlay.removeChild @matting[0]
@matting.shift()
if bounds.x is prev_bounds.x and bounds.y is prev_bounds.y and bounds.w is prev_bounds.w and bounds.h is prev_bounds.h
n = n.parent
continue
- matt = domify @outer_idoc, div: class: 'ann_box', style: "left: #{bounds.x - 1 + overlay_padding}px; top: #{bounds.y - 2 + overlay_padding}px; width: #{bounds.w}px; height: #{bounds.h}px" # outline: 1000px solid rgba(0,153,255,#{alpha});
- @overlay.appendChild matt
- @matting.push matt
- ann = domify @outer_idoc, div: class: 'ann_tag', style: "left: #{bounds.x + 1 + overlay_padding}px; top: #{bounds.y - 7 + overlay_padding}px", children: [domify @outer_idoc, text: " #{n.name} "]
- @overlay.appendChild ann
- @matting.push ann
+ ann_box = domify @outer_idoc, div: class: 'ann_box', style: "left: #{bounds.x - 1 + overlay_padding}px; top: #{bounds.y - 2 + overlay_padding}px; width: #{bounds.w}px; height: #{bounds.h}px" # outline: 1000px solid rgba(0,153,255,#{alpha});
+ @overlay.appendChild ann_box
+ @matting.push ann_box
+ ann_tag = domify @outer_idoc, div: class: 'ann_tag', style: "left: #{bounds.x + 1 + overlay_padding}px; top: #{bounds.y - 7 + overlay_padding}px", children: [domify @outer_idoc, text: " #{n.name} "]
+ @overlay.appendChild ann_tag
+ @matting.push ann_tag
n = n.parent
alpha *= 1.5
pretty_html: (tree, indent = '', parent_flags = pre_ish: false, block: true, want_nl: false) ->