# SETTINGS
overlay_padding = 10
+breathing_room = 30 # minimum pixels above/below cursor
timeout = (ms, cb) -> return setTimeout cb, ms
next_frame = (cb) ->
return false
return found # maybe null
+find_up_cursor_position = (tree, cursor, ideal_x) ->
+ new_cursor = cursor
+ # go prev until we're higher on y axis
+ while new_cursor.y >= cursor.y
+ new_cursor = find_prev_cursor_position tree, new_cursor
+ return null unless new_cursor?
+ # done early if we're already left of old cursor position
+ if new_cursor.x <= ideal_x
+ return new_cursor
+ target_y = new_cursor.y
+ # search leftward, until we find the closest position
+ # new_cursor is the prev-most position we've checked
+ # prev_cursor is the older value, so it's not as prev as new_cursor
+ while new_cursor.x > ideal_x and new_cursor.y is target_y
+ prev_cursor = new_cursor
+ new_cursor = find_prev_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 (ideal_x - new_cursor.x) < (prev_cursor.x - ideal_x)
+ return new_cursor
+ else
+ return prev_cursor
+ else
+ # new_cursor on wrong line, use prev_cursor
+ return prev_cursor
+ else
+ # can't go any further prev, use prev_cursor
+ return prev_cursor
+
+find_down_cursor_position = (tree, cursor, ideal_x) ->
+ 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 null unless new_cursor?
+ # done early if we're already right of old cursor position
+ if new_cursor.x >= ideal_x
+ # this would be strange, but could happen due to runaround
+ return new_cursor
+ 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 < ideal_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 - ideal_x) < (ideal_x - prev_cursor.x)
+ return new_cursor
+ else
+ return prev_cursor
+ else
+ # new_cursor on wrong line, use prev_cursor
+ return prev_cursor
+ else
+ # can't go any further prev, use prev_cursor
+ return prev_cursor
+
xy_to_cursor = (tree, xy) ->
for n in tree
if n.type is 'tag' or n.type is 'text'
constructor: (in_el, options) ->
@options = options ? {}
@in_el = in_el
- @tree = null
+ @tree = null # array of Nodes, all editable content
+ @tree_parent = null # @tree is this.children. .el might === @idoc.body
@matting = []
@init_1_called = false # when iframes have loaded
@outer_iframe # iframe to hold editor
@cursor = null
@cursor_el = null
@cursor_visible = false
+ @cursor_ideal_x = null
@poll_for_blur_timeout = null
opt_fragment = @options.fragment ? true
@parser_opts = {}
return false
when KEY_UP
if @cursor?
- new_cursor = @cursor
- # go prev until we're higher on y axis
- while new_cursor.y >= @cursor.y
- new_cursor = find_prev_cursor_position @tree, new_cursor
- return false unless new_cursor?
- # done early if we're already left of old cursor position
- if new_cursor.x <= @cursor.x
- @move_cursor new_cursor
- return false
- target_y = new_cursor.y
- # search leftward, until we find the closest position
- # new_cursor is the prev-most position we've checked
- # prev_cursor is the older value, so it's not as prev as new_cursor
- while new_cursor.x > @cursor.x and new_cursor.y is target_y
- prev_cursor = new_cursor
- new_cursor = find_prev_cursor_position @tree, new_cursor
- break unless new_cursor?
- # move cursor to prev_cursor or new_cursor
+ new_cursor = find_up_cursor_position @tree, @cursor, @cursor_ideal_x
if new_cursor?
- if new_cursor.y is target_y
- # both valid, and on the same line, use closest
- if (@cursor.x - new_cursor.x) < (prev_cursor.x - @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
+ saved_ideal_x = @cursor_ideal_x
+ @move_cursor new_cursor
+ @cursor_ideal_x = saved_ideal_x
else
# move cursor to first position in document
new_cursor = first_cursor_position @tree
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
+ new_cursor = find_down_cursor_position @tree, @cursor, @cursor_ideal_x
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
+ saved_ideal_x = @cursor_ideal_x
+ @move_cursor new_cursor
+ @cursor_ideal_x = saved_ideal_x
else
# move cursor to first position in document
new_cursor = last_cursor_position @tree
@move_cursor new_cursor
return false
when KEY_END
- return false
- when KEY_BACKSPACE
- return false unless @cursor?
- return false unless @cursor.i > 0
- @remove_character @cursor.n, @cursor.i - 1
- @adjust_whitespace_style @cursor.n
- @changed()
- new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1
+ new_cursor = last_cursor_position @tree
if new_cursor?
@move_cursor new_cursor
- else
- @kill_cursor()
+ return false
+ when KEY_BACKSPACE
+ @on_key_backspace e
return false
when KEY_DELETE
return false unless @cursor?
@kill_cursor()
return false
when KEY_ENTER
+ @on_key_enter e
return false
when KEY_ESCAPE
+ @kill_cursor()
return false
when KEY_HOME
+ new_cursor = first_cursor_position @tree
+ if new_cursor?
+ @move_cursor new_cursor
return false
when KEY_INSERT
return false
when KEY_PAGE_UP
+ @on_page_up_key e
return false
when KEY_PAGE_DOWN
+ @on_page_down_key e
return false
when KEY_TAB
return false
console.log "ERROR: couldn't find cursor position after insert"
@kill_cursor()
return false
+ on_key_enter: (e) -> # enter key pressed
+ return unless @cursor_visible
+ cur_block = @cursor.n
+ loop
+ if cur_block.type is 'tag'
+ if is_display_block cur_block.el
+ break
+ return unless cur_block.parent?
+ cur_block = cur_block.parent
+ # find array to insert new element into
+ if cur_block.parent is @tree_parent # top-level
+ parent_el = @idoc.body
+ pc = @tree
+ else
+ parent_el = cur_block.parent.el
+ pc = cur_block.parent.children
+ # find index of current block in its parent
+ for n, i in pc
+ break if n is cur_block
+ i += 1 # we want to be after it
+ if i < pc.length
+ before = pc[i].el
+ else
+ before = null
+ # TODO if content after cursor
+ # TODO new block is empty
+ new_text = new peach_parser.Node 'text', text: ' '
+ new_node = new peach_parser.Node 'tag', name: 'p', parent: cur_block.parent, attrs: {style: 'white-space: pre-wrap'}, children: [new_text]
+ new_text.parent = new_node
+ new_text.el = domify @idoc, text: ' '
+ new_node.el = domify @idoc, p: style: 'white-space: pre-wrap', children: [new_text.el]
+ pc.splice i, 0, new_node
+ parent_el.insertBefore new_node.el, before
+ @changed()
+ new_cursor = new_cursor_position n: new_text, i: 0
+ throw 'bork bork' unless new_cursor?
+ @move_cursor new_cursor
+ # TODO move content past cursor into this new block
+ on_key_backspace: (e) ->
+ return false unless @cursor?
+ if @is_lone_space @cursor.n # false if it's not in a tag
+ if @cursor.i is 1
+ # don't delete the space, because then it would collapse
+ # instead leave a space after the cursor
+ new_cursor = new_cursor_position n: @cursor.n, i: 0
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor()
+ else
+ parent = @cursor.n.parent
+ new_cursor = find_prev_cursor_position @tree, @cursor
+ if new_cursor?
+ if new_cursor.n is @cursor.n or new_cursor.n is parent
+ new_cursor = null
+ tag = @cursor.n.parent
+ if tag is @tree_parent
+ console.log "top-level text not supported" # FIXME
+ return false
+ for n, i in tag.parent.children
+ if n is tag
+ tag.parent.el.removeChild tag.el
+ tag.parent.children.splice i, 1
+ break
+ @changed()
+ if new_cursor?
+ # re-check, in case it moved or is invalid now
+ new_cursor = new_cursor_position n: new_cursor.n, i: new_cursor.i
+ if new_cursor?
+ @move_cursor new_cursor
+ return
+ new_cursor = first_cursor_position @tree
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor
+ return
+ else if @cursor.i is 0
+ console.log 'unimplemented: backspace at start of non-empty tag'
+ # TODO if block, merge parent into prev
+ # TODO if inline, delete char from prev text node
+ return false
+ else
+ # TODO handle case of removing last char
+ # CONTINUE
+ if @is_only_char_in_tag @cursor.n
+ if is_display_block @cursor.n.parent.el
+ @cursor.n.el.textContent = @cursor.n.text = ' '
+ else
+ console.log "unimplemented: delete last char in inline" # FIXME
+ return
+ else
+ @remove_character @cursor.n, @cursor.i - 1
+ @adjust_whitespace_style @cursor.n
+ @changed()
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor()
+ return
+ on_page_up_key: (e) ->
+ if @cursor?
+ screen_y = @cursor.y - @wrap2.scrollTop
+ scroll_amount = @wrap2_height - breathing_room
+ @wrap2.scrollTop = Math.max 0, @wrap2.scrollTop - scroll_amount
+ if @cursor?
+ @move_cursor_into_view screen_y + @wrap2.scrollTop
+ on_page_down_key: (e) ->
+ if @cursor?
+ screen_y = @cursor.y - @wrap2.scrollTop
+ scroll_amount = @wrap2_height - breathing_room
+ lowest_scrollpos = @wrap2.scrollHeight - @wrap2_height
+ @wrap2.scrollTop = Math.min lowest_scrollpos, @wrap2.scrollTop + scroll_amount
+ if @cursor?
+ @move_cursor_into_view screen_y + @wrap2.scrollTop
+ return
+ move_cursor_into_view: (y_target) ->
+ return if y_target is @cursor.y
+ was = @cursor
+ y_min = @wrap2.scrollTop + breathing_room
+ y_max = @wrap2.scrollTop + @wrap2_height - 2 * breathing_room
+ y_target = Math.min y_target, y_max
+ y_target = Math.max y_target, y_min
+ if y_target < @cursor.y
+ finder = find_up_cursor_position
+ far_enough = (a, b) ->
+ return a <= b
+ else
+ finder = find_down_cursor_position
+ far_enough = (a, b) ->
+ return a >= b
+ loop
+ cur = finder @tree, was, @cursor_ideal_x
+ break unless cur?
+ break if far_enough cur.y, y_target
+ was = cur
+ if was is @cursor
+ was = null
+ if was?
+ if was.y > y_max
+ was = null
+ else if was.y < y_min
+ was = null
+ if cur?
+ if cur.y > y_max
+ cur = null
+ else if cur.y < y_min
+ cur = null
+ if cur? and was?
+ # both valid, pick best
+ if cur.y < y_min
+ new_cursor = was
+ else if was.y > y_max
+ new_cursor = cur
+ else if cur.y - y_target < y_target - was.y
+ new_cursor = cur
+ else
+ new_cursor = was
+ else
+ new_cursor = was ? cur
+ if new_cursor?
+ saved_ideal_x = @cursor_ideal_x
+ @move_cursor new_cursor
+ @cursor_ideal_x = saved_ideal_x
+ return
clear_dom: -> # remove all the editable content (and cursor, overlays, etc)
while @idoc.body.childNodes.length
@idoc.body.removeChild @idoc.body.childNodes[0]
return
load_html: (html) ->
@tree = peach_parser.parse html, @parser_opts
+ if !@tree[0]?.parent
+ @tree = peach_parser.parse '<p style="white-space: pre-wrap"> </p>', @parser_opts
+ @tree_parent = @tree[0]?.parent
+ @tree_parent.el = @idoc.body
@clear_dom()
- instantiate_tree @tree, @idoc.body
+ instantiate_tree @tree, @tree_parent.el
tree_dedup_space @tree
@changed()
changed: ->
if needle is n.attrs.style.substr n.attrs.style.length - needle
n.attrs.style = n.attrs.style.substr 0, n.attrs.style.length - needle
n.el.setAttribute n.attrs.style
+ # true if n is text node with only one caracter, and the only child of a tag
+ is_only_char_in_tag: (n, i) ->
+ return false unless n.type is 'text'
+ return false unless n.text.length is 1
+ return false if n.parent is @tree_parent
+ return false unless n.parent.children.length is 1
+ return true
+ # true if n is text node with just a space in it, and the only child of a tag
+ is_lone_space: (n, i) ->
+ return false unless n.type is 'text'
+ return false unless n.text is ' '
+ return false if n.parent is @tree_parent
+ return false unless n.parent.children.length is 1
+ return true
+ # detect special case: typing before a space that's the only thing in a block/doc
+ # reason: enter key creates blocks with just a space in them
+ insert_should_replace: (n, i) ->
+ return false unless i is 0
+ return false unless n.text is ' '
+ return true if n.parent is @tree_parent
+ if n.parent.children.length is 1
+ if n.parent.children[0] is n
+ # n is only child
+ return true
+ return false
# after calling this, you MUST call changed() and adjust_whitespace_style()
insert_character: (n, i, char) ->
+ return if @cursor.n.parent is @tree_parent # FIXME implement text nodes at top level
parent = @cursor.n.parent
- return unless parent
- return unless parent.el?
# insert the character
- if i is 0
+ if @insert_should_replace n, i
+ n.text = char
+ else if i is 0
n.text = char + n.text
else if i is n.text.length
+ # replace the space
n.text += char
else
n.text =
@cursor = null
@annotate null
move_cursor: (cursor) ->
+ @cursor_ideal_x = cursor.x
@cursor = cursor
unless @cursor_visible
@cursor_el = domify @outer_idoc, div: id: 'cursor'
@annotate cursor.n
@scroll_into_view cursor.y, height
scroll_into_view: (y, h = 0) ->
- closest = 30 # setting: smallest pixels from top/bottom of screet that's OK
y += overlay_padding # convert units from @idoc to @wrap2
# very top of document
- if y <= closest
+ if y <= breathing_room
@wrap2.scrollTop = 0
return
# very bottom of document
- if y + h >= @wrap2.scrollHeight - closest
+ if y + h >= @wrap2.scrollHeight - breathing_room
@wrap2.scrollTop = @wrap2.scrollHeight - @wrap2_height
return
# The most scrolled up (lowest value for scrollTop) that would be OK
- upmost = y + h + closest - @wrap2_height
+ upmost = y + h + breathing_room - @wrap2_height
upmost = Math.max(upmost, 0)
# the most scrolled down (highest value for scrollTop) that would be OK
- downmost = y - closest
+ downmost = y - breathing_room
downmost = Math.min(downmost, @wrap2.scrollHeight - @wrap2_height)
if upmost > downmost # means h is too big to fit
# scroll so top is visible
return unless n?
prev_bounds = x: 0, y: 0, w: 0, h: 0
alpha = 0.1
- while n?.el?
+ while n?.el? and n isnt @tree_parent
if n.type is 'text'
n = n.parent
continue