JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
replace del key hack with one that uses backspace code
[peach-html5-editor.git] / editor.coffee
index ec9f9f9..238625c 100644 (file)
@@ -16,7 +16,7 @@
 
 # SETTINGS
 overlay_padding = 10
-breathing_room = 30 # minimum pixels above/below cursor
+breathing_room = 30 # minimum pixels above/below cursor (scrolling)
 
 timeout = (ms, cb) -> return setTimeout cb, ms
 next_frame = (cb) ->
@@ -73,6 +73,9 @@ js_attr_regex = new RegExp '^[oO][nN].'
 # html5 spec says that only these characters are collapsable
 multi_sp_regex = new RegExp '[\u0020\u0009\u000a\u000c\u000d][\u0020\u0009\u000a\u000c\u000d]'
 
+str_has_ws_run = (str) ->
+       return multi_sp_regex.test str
+
 debug_dot_at = (doc, x, y) ->
        return # disabled
        el = doc.createElement 'div'
@@ -949,15 +952,22 @@ class PeachHTML5Editor
                                return false
                        when KEY_DELETE
                                return false unless @cursor?
-                               return false unless @cursor.i < @cursor.n.text.length
-                               @remove_character @cursor.n, @cursor.i
-                               @text_cleanup @cursor.n
-                               @changed()
-                               new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i
+                               new_cursor = find_next_cursor_position @tree, n: @cursor.n, i: @cursor.i
+                               # try moving cursor right and then running backspace code
+                               # TODO replace this hack with a real implementation
                                if new_cursor?
-                                       @move_cursor new_cursor
-                               else
-                                       @kill_cursor()
+                                       # try to detect common case where cursor goes inside an block,
+                                       # but doesn't pass a character (and advance one more in that case)
+                                       if new_cursor.n isnt @cursor.n and new_cursor.i is 0
+                                               if new_cursor.n.type is 'text' and new_cursor.n.text.length > 0
+                                                       if new_cursor.n.parent?
+                                                               unless @is_display_block new_cursor.n.parent
+                                                                       # FIXME should test run sibling
+                                                                       new_cursor = new_cursor_position n: new_cursor.n, i: new_cursor.i + 1
+                               if new_cursor?
+                                       if new_cursor.n isnt @cursor.n or new_cursor.i isnt @cursor.i
+                                               @move_cursor new_cursor
+                                               @on_key_backspace e
                                return false
                        when KEY_ENTER
                                @on_key_enter e
@@ -1050,13 +1060,13 @@ class PeachHTML5Editor
        # that are flowing/wrapping together. n can be the containing block, or any
        # element inside it.
        get_text_run: (n) ->
+               ret = []
                if @is_display_block n
                        block = n
                else
                        block = @find_block_parent n
-                       return unless block?
-               ret = []
-               traverse_tree n.children, (n) =>
+                       return ret unless block?
+               traverse_tree block.children, (n) =>
                        if n.type is 'text'
                                ret.push n
                        else if n.type is 'tag'
@@ -1068,135 +1078,179 @@ class PeachHTML5Editor
                                                ret.push n
                        return false # not done traversing
                return ret
+       node_is_decendant: (young, old) ->
+               while young? and young != @tree_parent
+                       return true if young is old
+                       young = young.parent
+               return false
+       # helper for on_key_backspace
+       _merge_left: (state) ->
+               # the node prev to n was not prev to it a moment ago, merge with it if reasonable
+               pi = state.n.parent.children.indexOf(state.n)
+               if pi > 0
+                       prev = state.n.parent.children[pi - 1]
+                       if prev.type is 'text'
+                               state.i = prev.text.length
+                               prev.text = prev.el.textContent = prev.text + state.n.text
+                               @remove_node state.n
+                               state.n = prev
+                               state.changed = true
+                               state.moved_cursor = true
+               # else # TODO merge possible consecutive matching inline tags at @cursor
+               return state
+       # helper for on_key_backspace
+       # remove n from the dom, also remove its inline parents that are emptied by removing n
+       _backspace_node_helper: (n, run = @get_text_run(n), run_i = run.indexOf(n)) ->
+               block = @find_block_parent n
+               # delete text node
+               @remove_node n
+               # delete any inline parents
+               n = n.parent
+               while n? and n isnt block
+                       # bail if the previous node in this run is also inside the same parent
+                       if run_i > 0
+                               break if @node_is_decendant run[run_i - 1], n
+                       # bail if the next node in this run is also inside the same parent
+                       if run_i + 1 < run.length
+                               break if @node_is_decendant run[run_i + 1], n
+                       # move any sibling nodes to parent. These nodes are not in the text run
+                       while n.children.length > 0
+                               @move_node n.children[0], n.parent, n
+                       # remove (now completely empty) inline parent
+                       @remove_node n
+                       # proceed to outer parent
+                       n = n.parent
+               return
        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
-                               # cursor at the begining of an element that contains only a space
-                               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
+               return unless @cursor?
+               new_cursor = null
+               run = null
+               changed = true
+               if @cursor.i is 0 # cursor is at start of text node
+                       run ?= @get_text_run @cursor.n
+                       run_i = run.indexOf(@cursor.n)
+                       if run_i is 0 # if at start of text run
+                               block = @find_block_parent @cursor.n
+                               prev_cursor = find_prev_cursor_position @tree, n: @cursor.n, i: 0
+                               if prev_cursor is null # if in first text run of document
+                                       # do nothing (there's nothing text-like to the left of the cursor)
+                                       return
+                               # else merge with prev/outer text run
+                               pcb = @find_block_parent prev_cursor.n
+                               while block.children.length > 0
+                                       @move_node block.children[0], pcb
+                               @remove_node block
+                               # merge possible consecutive text nodes at @cursor
+                               merge_state = n: @cursor.n
+                               @_merge_left merge_state
+                               @text_cleanup merge_state.n
+                               new_cursor = new_cursor_position n: merge_state.n, i: merge_state.i
+                       else # at start of text node, but not start of text run
+                               prev = run[run_i - 1]
+                               if prev.type is 'text' # if previous in text run is text
+                                       if prev.text.length is 1 # if emptying prev (in text run)
+                                               @_backspace_node_helper prev, run, run_i
+                                               merge_state = n: @cursor.n, i: @cursor.i
+                                               @_merge_left merge_state
+                                               @text_cleanup merge_state.n
+                                               new_cursor = new_cursor_position n: merge_state.n, i: merge_state.i
+                                       else # prev in run is text with muliple chars
+                                               # delete last character in prev
+                                               prev.text = prev.text.substr(0, prev.text.length - 1)
+                                               prev.el.textContent = prev.text
+                                               @text_cleanup @cursor.n
+                                               new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i
+                               else if prev.name is 'br' or prev.name is 'hr'
+                                       @_backspace_node_helper prev, run, run_i
+                                       merge_state = n: @cursor.n, i: @cursor.i
+                                       @_merge_left merge_state
+                                       @text_cleanup merge_state.n
+                                       new_cursor = new_cursor_position n: merge_state.n, i: merge_state.i
+                               # FIXME implement this:
+                               # else # if prev (in run) is inline-block
+                                       # if that inline-block has text in it
+                                               # delete last char in prev inlineblock
+                                               # if that empties it
+                                                       # delete it
+                                                       # merge left
+                                               # else
+                                                       # move cursor inside
+                                       # else
+                                               # delete prev (inline) block
+                                               # merge left
+                                       # auto-delete this @cursor.parent(s) if this empties them
+               else # cursor is not at start of text node
+                       run ?= @get_text_run @cursor.n
+                       if @cursor.n.text.length is 1 # if emptying text node
+                               if run.length is 1 # if emptying text run (of text/br/hr/inline-block)
+                                       # remove inline-parents of @cursor.n
+                                       block = @find_block_parent @cursor.n
+                                       changed = false
+                                       n = @cursor.n.parent
+                                       # note: this doesn't use _backspace_node_helper because:
+                                       # 1. we don't want to delete the target node (we're replacing it's contents)
+                                       # 2. we want to track whether anything was removed
+                                       # 3. we know already know there's no other text from this run anywhere
+                                       while n and n isnt block
+                                               changed = true
+                                               while n.children.length > 0
+                                                       @move_node n.children[0], n.parent, n
+                                               @remove_node n
+                                               n = n.parent
+                                       # replace @cursor.n with a single (preserved) space
+                                       if @cursor.n.text != ' '
+                                               changed = true
+                                               @cursor.n.text = @cursor.n.el.textContent = ' '
+                                       if changed
+                                               @text_cleanup @cursor.n
+                                       # place the cursor to the left of that space
+                                       new_cursor = new_cursor_position n: @cursor.n, i: 0
+                               else # emptying a text node (but not a whole text run)
+                                       # figure out where cursor should land
+                                       block = @find_block_parent @cursor.n
+                                       new_cursor = find_prev_cursor_position @tree, n: @cursor.n, i: 0
+                                       ncb = @find_block_parent new_cursor.n
+                                       if ncb isnt block
+                                               new_cursor = find_next_cursor_position @tree, n: @cursor.n, i: 1
+                                       # delete text node and cleanup emptied parents
+                                       run_i = run.indexOf @cursor.n
+                                       @_backspace_node_helper @cursor.n, run, run_i
+                                       # see if new adjacent siblings should merge
+                                       # TODO make smarter
+                                       if run_i > 0 and run_i + 1 < run.length
+                                               if run[run_i - 1].type is 'text' and run[run_i + 1].type is 'text'
+                                                       merge_state = n: run[run_i + 1]
+                                                       @_merge_left merge_state
+                                                       if merge_state.moved_cursor
+                                                               new_cursor = merge_state
+                                       # update whitespace preservation
+                                       @text_cleanup(block)
+                                       # update cursor x/y in case things moved around
                                        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 # start of text chunk
-                       # FIXME clean this up: use new code for text runs
-                       # FIXME handle backspacing a <br> even if it's near a inline tag boundary
-                       # determine if cursor is at start of text run (text formatted inline)
-                       block = @find_block_parent @cursor.n
-                       return unless block
-                       at_block_start = true
-                       prev_pos = find_prev_cursor_position @tree, @cursor
-                       unless prev_pos?
-                               # if the cursor can't go back, then there's probably nowhere we can merge into
-                               # TODO consider case of nested blocks. should backspace remove one?
-                               return
-                       prev_pos_block = @find_block_parent prev_pos.n
-                       if prev_pos_block is block
-                               # context: there is text before the cursor within the same block.
-                               # FIXME clean up this hack for looking for <br> (see above)
-                               cursor_text_pi = @cursor.n.parent.children.indexOf @cursor.n
-                               if cursor_text_pi > 0
-                                       prev_node = @cursor.n.parent.children[cursor_text_pi - 1]
-                                       if prev_node.type is 'tag' and prev_node.name is 'br'
-                                               @remove_node prev_node
-                                               @text_cleanup @cursor.n.parent
-                                               @changed()
-                                               new_cursor = new_cursor_position n: prev_pos.n, i: prev_pos.i
-                                               if new_cursor?
-                                                       @move_cursor new_cursor
+                                               if new_cursor.n.el.parentNode # still in dom after cleanup
+                                                       new_cursor = new_cursor_position n: new_cursor.n, i: new_cursor.i
                                                else
-                                                       @kill_cursor
-                                               return
-                               # note: find_prev_cursor_position just crossed a boundary, not a character
-                               # prev_pos is within the same block, try deleting there
-                               @move_cursor prev_pos
-                               # FIXME cleanup: don't call @move_cursor twice if the next line succeeds
-                               return @on_key_backspace()
-                       # context: backspace pressed at start of a display:block
-                       return if block is @tree_parent # top level text
-                       parent = block.parent
-                       parent_i = parent.children.indexOf block
-                       if parent_i is -1
-                               throw "BUG #98270918347"
-                               return
-                       if parent_i is 0
-                               # no previous sibling to merge into, so instead move contents into parent
-                               dest = parent
-                               before = block
-                       else
-                               # FIXME prev_sib should be the previous in-flow element
-                               # ie it should skip comments, hidden things, floating things, etc.
-                               prev_sib = parent.children[parent_i - 1]
-                               if @is_display_block prev_sib
-                                       dest = prev_sib
-                                       before = null # null means append
-                               else
-                                       dest = parent
-                                       before = block
-                       if dest is @tree_parent
-                               # don't remove outer-most blocks
-                               return
-                       while block.children.length > 0
-                               n = block.children[block.children.length - 1]
-                               @move_node n, dest, before
-                               before = n
-                       @remove_node block
-                       @text_cleanup dest
+                                                       new_cursor = null
+                       else # there's a char left of cursor that we can delete without emptying anything
+                               # delete character
+                               need_text_cleanup = true
+                               if @cursor.i > 1 and @cursor.i < @cursor.n.text.length
+                                       pre = @cursor.n.text.substr(@cursor.i - 2, 3)
+                                       post = pre.charAt(0) + pre.charAt(2)
+                                       if str_has_ws_run(pre) is str_has_ws_run(post)
+                                               need_text_cleanup = false
+                               @remove_character(@cursor.n, @cursor.i - 1)
+                               # call text_cleanup if whe created/removed a whitespace run
+                               if need_text_cleanup
+                                       @text_cleanup @cursor.n
+                               new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1
+               # mark document changed and move the cursor
+               if changed?
                        @changed()
-                       new_cursor = new_cursor_position n: prev_pos.n, i: prev_pos.i
-                       if new_cursor?
-                               @move_cursor new_cursor
-                       else
-                               @kill_cursor
-                       return
+               if new_cursor?
+                       @move_cursor new_cursor
                else
-                       # TODO handle case of removing last char
-                       # CONTINUE
-                       if @is_only_char_in_tag @cursor.n
-                               if @is_display_block @cursor.n.parent
-                                       @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
-                       @text_cleanup @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()
+                       @kill_cursor()
                return
        on_page_up_key: (e) ->
                if @wrap2.scrollTop is 0