JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
rewrite backspace to use text runs/etc
authorJason Woofenden <jason@jasonwoof.com>
Tue, 24 May 2016 21:51:00 +0000 (17:51 -0400)
committerJason Woofenden <jason@jasonwoof.com>
Tue, 24 May 2016 21:51:00 +0000 (17:51 -0400)
editor.coffee

index ec9f9f9..8835a33 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'
@@ -1050,13 +1053,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 +1071,155 @@ class PeachHTML5Editor
                                                ret.push n
                        return false # not done traversing
                return ret
+       _merge_left: (state) -> # helper for on_key_backspace
+               # 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
+               # else # TODO merge possible consecutive matching inline tags at @cursor
+               return state
+       _remove_node_and_inline_parents: (n) -> # helper for on_key_backspace
+               block = @find_block_parent n
+               # delete text node
+               @remove_node n
+               # delete any inline parents
+               n = n.parent
+               while n? and n isnt block
+                       while n.children.length > 0
+                               @move_node n.children[0], n.parent, n
+                       @remove_node n
+                       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
-                                       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 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 -1
+                               console.log 'run', run
+                               console.log 'cursor', @cursor
+                               throw 'fffffff'
                                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 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)
+                                               @_remove_node_and_inline_parents prev
+                                               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'
+                                       @_remove_node_and_inline_parents prev
+                                       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 CONTINUE
+                               # 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 n
+                                               changed = false
+                                               n = @cursor.n.parent
+                                               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
+                                               if ncb isnt block
+                                                       new_cursor = find_next_cursor_position @tree, n: @cursor.n, i: 1
+                                               # delete text node
+                                               @remove_node @cursor.n
+                                               # delete any inline parents
+                                               n = @cursor.n.parent
+                                               while n and n isnt block
+                                                       while n.children.length > 0
+                                                               @move_node n.children[0], n.parent, n
+                                                       @remove_node n
+                                                       n = n.parent
+                                               # update cursor dest in case things moved around
                                                if new_cursor?
-                                                       @move_cursor new_cursor
-                                               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 = new_cursor_position n: new_cursor.n, i: new_cursor.i
+                       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