From 7f075c8f4e23d2c49dbfadf8819ca61630466119 Mon Sep 17 00:00:00 2001 From: Jason Woofenden Date: Fri, 18 Mar 2016 13:48:53 -0400 Subject: [PATCH] create/use CursorPosition class (includes x,y) --- editor.coffee | 321 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 164 insertions(+), 157 deletions(-) diff --git a/editor.coffee b/editor.coffee index 8290b37..3d3336d 100644 --- a/editor.coffee +++ b/editor.coffee @@ -111,22 +111,42 @@ text_range_bounds = (el, start, end) -> bounding: range.getBoundingClientRect() } -# figure out the x/y coordinates of where the cursor should be if it's at -# position ``i`` within text node ``n`` -# sometimes returns null (eg for whitespace that is not visible) -window.cursor_to_xyh = cursor_to_xyh = (n, i) -> - range = document.createRange() - if n.text.length is 0 - ret = text_range_bounds n.el, 0, 0 - else if i is n.text.length - ret = text_range_bounds n.el, i - 1, i +class CursorPosition + constructor: (args) -> + @n = args.n ? null + @i = args.i ? null + if args.x? + @x = args.x + @y = args.y + @h = args.h + else + @set_xyh() + return + set_xyh: -> + range = document.createRange() + if @n.text.length is 0 + ret = text_range_bounds @n.el, 0, 0 + else if @i is @n.text.length + ret = text_range_bounds @n.el, @i - 1, @i + if ret? + ret.x += ret.w + else + ret = text_range_bounds @n.el, @i, @i + 1 if ret? - ret.x += ret.w - else - ret = text_range_bounds n.el, i, i + 1 - if ret? - debug_dot_at n.el.ownerDocument, ret.x, ret.y - return ret + @x = ret.x + @y = ret.y + @h = ret.h + else + @x = null + @y = null + @h = null + return ret + +new_cursor_position = (args) -> + ret = new CursorPosition args + if ret.x? + return ret + return null # encode text so it can be safely placed inside an html attribute enc_attr_regex = new RegExp '(&)|(")|(\u00A0)', 'g' @@ -351,114 +371,117 @@ traverse_tree = (tree, cb) -> return done if done return done -find_next_cursor_position = (tree, n, i) -> - if n.type is 'text' and n.text.length > i - orig_xyh = cursor_to_xyh n, i - unless orig_xyh? - console.log "ERROR: couldn't find xy for current cursor location" - return - for next_i in [i+1 .. n.text.length] # inclusive is valid (after last char) - next_xyh = cursor_to_xyh n, next_i - if next_xyh? - if next_xyh.x > orig_xyh.x or next_xyh.y > orig_xyh.y - return [n, next_i] +first_cursor_position = (tree) -> + found = null + traverse_tree tree, (node, state) -> + if node.type is 'text' + cursor = new_cursor_position n: node, i: 0 + if cursor? + found = cursor + return true + return false + return found # maybe null + +# this will fail when text has non-locatable cursor positions +find_next_cursor_position = (tree, cursor) -> + if cursor.n.type is 'text' and cursor.n.text.length > cursor.i + new_cursor = new_cursor_position n: cursor.n, i: cursor.i + 1 + if new_cursor? + return new_cursor state_before = true found = null traverse_tree tree, (node, state) -> if node.type is 'text' and state_before is false - if cursor_to_xyh(node, 0)? - found = node + new_cursor = new_cursor_position n: node, i: 0 + if new_cursor? + found = new_cursor return true - if node is n + if node is cursor.n state_before = false return false if found? - return [found, 0] + return found return null -find_prev_cursor_position = (tree, n, i) -> - if n? and n.type is 'text' and i > 0 - orig_xyh = cursor_to_xyh n, i - unless orig_xyh? - console.log "ERROR: couldn't find xy for current cursor location" - return - for prev_i in [i-1 .. 0] - prev_xyh = cursor_to_xyh n, prev_i - if prev_xyh? - if prev_xyh.x < orig_xyh.x or prev_xyh.y < orig_xyh.y - return [n, prev_i] - return [n, i - 1] +last_cursor_position = (tree) -> + found = null + traverse_tree tree, (node) -> + if node.type is 'text' + cursor = new_cursor_position n: node, i: node.text.length + if cursor? + found = cursor + return false + return found # maybe null + +# this will fail when text has non-locatable cursor positions +find_prev_cursor_position = (tree, cursor) -> + if cursor.n.type is 'text' and cursor.i > 0 + new_cursor = new_cursor_position n: cursor.n, i: cursor.i - 1 + if new_cursor? + return new_cursor found_prev = null found = null traverse_tree tree, (node) -> + if node is cursor.n + found = found_prev # maybe null + return true if node.type is 'text' - unless n? - found = node - return true - if node is n - found = found_prev # null if n is the first text node - return true - found_prev = node + new_cursor = new_cursor_position n: node, i: node.text.length + if new_cursor? + found_prev = new_cursor return false - if found? - if cursor_to_xyh found, found.text.length # text visible? - return [found, found.text.length] - return find_prev_cursor_position tree, found, 0 - return null + return found # maybe null -find_loc_cursor_position = (tree, loc) -> - for c in tree - if c.type is 'tag' or c.type is 'text' - bounds = get_el_bounds c.el - continue if loc.x < bounds.x - continue if loc.x > bounds.x + bounds.w - continue if loc.y < bounds.y - continue if loc.y > bounds.y + bounds.h - if c.children.length - ret = find_loc_cursor_position c.children, loc +xy_to_cursor = (tree, xy) -> + for n in tree + if n.type is 'tag' or n.type is 'text' + bounds = get_el_bounds n.el + continue if xy.x < bounds.x + continue if xy.x > bounds.x + bounds.w + continue if xy.y < bounds.y + continue if xy.y > bounds.y + bounds.h + if n.children.length + ret = xy_to_cursor n.children, xy return ret if ret? - if c.type is 'text' + if n.type is 'text' # click is within bounding box that contains all text. - return [c, 0] if c.text.length is 0 - before_i = 0 - before = cursor_to_xyh c, before_i - unless before? - console.log "error: failed to find cursor pixel location for start of", c - return - after_i = c.text.length - after = cursor_to_xyh c, after_i - unless after? - console.log "error: failed to find cursor pixel location for end of", c - return - if loc.y < before.y + before.h and loc.x < before.x + if n.text.length is 0 + ret = new_cursor_position n: n, i: 0 + return ret if ret? + continue + before = new_cursor_position n: n, i: 0 + continue unless before? + after = new_cursor_position n: n, i: n.text.length + continue unless after? + if xy.y < before.y + before.h and xy.x < before.x # console.log 'before first char on first line' continue - if loc.y > after.y and loc.x > after.x + if xy.y > after.y and xy.x > after.x # console.log 'after last char on last line' continue - if loc.y < before.y - console.log "Warning: click in bounding box but above first line" + if xy.y < before.y + console.log "Warning: click in text bounding box but above first line" continue # above first line (runaround?) - if loc.y > after.y + after.h - console.log "Warning: click in bounding box but below last line", loc.y, after.y, after.h + if xy.y > after.y + after.h + console.log "Warning: click in text bounding box but below last line", xy.y, after.y, after.h continue # below last line (shouldn't happen?) - while after_i - before_i > 1 - cur_i = Math.round((before_i + after_i) / 2) - cur = cursor_to_xyh c, cur_i - unless loc? - console.log "error: failed to find cursor pixel location for", c, cur_i - return - if loc.y < cur.y or (loc.y <= cur.y + cur.h and loc.x < cur.x) - after_i = cur_i + while after.i - before.i > 1 + guess_i = Math.round((before.i + after.i) / 2) + cur = new_cursor_position n: n, i: guess_i + unless cur? + console.log "error: failed to find cursor pixel location for", n, guess_i + before = null + break + if xy.y < cur.y or (xy.y <= cur.y + cur.h and xy.x < cur.x) after = cur else - before_i = cur_i before = cur + continue unless before? # signals failure to find a cursor position # which one is closest? - if Math.abs(before.x - loc.x) < Math.abs(after.x - loc.x) - return [c, before_i] + if Math.abs(before.x - xy.x) < Math.abs(after.x - xy.x) + return before else - return [c, after_i] + return after return null # browsers collapse these (html5 spec calls these "space characters") @@ -553,22 +576,22 @@ tree_dedup_space = (tree) -> # # remove it from the dom, and if prev or next moves, put it back. if prev? and not prev_px? - prev_px = cursor_to_xyh prev, prev_i + prev_px = new_cursor_position n: prev, i: prev_i if next? and not next_px? - next_px = cursor_to_xyh next, next_i + next_px = new_cursor_position n: next, i: next_i #if prev is null and next is null # parent_px = cur.parent.el.getBoundingClientRect() remove() if prev? if prev_px? - new_prev_px = cursor_to_xyh prev, prev_i + new_prev_px = new_cursor_position n: prev, i: prev_i if new_prev_px.x isnt prev_px.x or new_prev_px.y isnt prev_px.y return put_it_back() else console.log "this shouldn't happen, we remove spaces that don't locate" if next? if next_px? - new_next_px = cursor_to_xyh next, next_i + new_next_px = new_cursor_position n: next, i: next_i if new_next_px.x isnt next_px.x or new_next_px.y isnt next_px.y return put_it_back() #else @@ -710,7 +733,7 @@ class PeachHTML5Editor return x: x - @wrap2_offset.x, y: y - @wrap2_offset.y onclick: (e) -> xy = @overlay_event_to_inner_xy e - new_cursor = find_loc_cursor_position @tree, xy + new_cursor = xy_to_cursor @tree, xy if new_cursor? @move_cursor new_cursor else @@ -729,70 +752,58 @@ class PeachHTML5Editor switch e.keyCode when KEY_LEFT if @cursor? - new_cursor = find_prev_cursor_position @tree, @cursor... - if new_cursor? - @move_cursor new_cursor + new_cursor = find_prev_cursor_position @tree, @cursor else - for c in @tree - new_cursor = find_next_cursor_position @tree, c, -1 - if new_cursor? - @move_cursor new_cursor - break + new_cursor = first_cursor_position @tree + if new_cursor? + @move_cursor new_cursor + return false + when KEY_RIGHT + if @cursor? + new_cursor = find_next_cursor_position @tree, @cursor + else + new_cursor = last_cursor_position @tree + if new_cursor? + @move_cursor new_cursor return false when KEY_UP if @cursor? new_cursor = @cursor - old_loc = cursor_to_xyh new_cursor[0], new_cursor[1] - new_loc = x: old_loc.x, y: old_loc.y - while new_loc.y >= old_loc.y - new_cursor = find_prev_cursor_position @tree, new_cursor[0], new_cursor[1] + # 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? - new_loc = cursor_to_xyh new_cursor[0], new_cursor[1] + # 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 if new_cursor? - # now we're above - if new_loc.x <= old_loc.x - @move_cursor new_cursor - return false - target_y = new_loc.y - # search leftward, until we find the closest position - while new_loc.x > old_loc.x and new_loc.y is target_y - prev_loc = new_loc - prev_cursor = new_cursor - new_cursor = find_prev_cursor_position @tree, new_cursor[0], new_cursor[1] - break unless new_cursor? - new_loc = cursor_to_xyh new_cursor[0], new_cursor[1] - # move cursor to prev_cursor or new_cursor - if new_cursor? - if new_loc.y is target_y - # both valid, and on the same line, use closest - if (old_loc.x - new_loc.x) < (prev_loc.x - old_loc.x) - @move_cursor new_cursor - else - @move_cursor prev_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 - # new_cursor on wrong line, use prev_cursor @move_cursor prev_cursor else - # can't go any further prev, use prev_cursor + # 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 = find_prev_cursor_position @tree - if new_cursor? - @move_cursor new_cursor - break - return false - when KEY_RIGHT - if @cursor? - new_cursor = find_next_cursor_position @tree, @cursor... - if new_cursor? - @move_cursor new_cursor - else - # move cursor to first position in document - new_cursor = find_prev_cursor_position @tree + new_cursor = first_cursor_position @tree if new_cursor? @move_cursor new_cursor - break return false when KEY_DOWN return false @@ -880,10 +891,6 @@ class PeachHTML5Editor @cursor = null @matt null move_cursor: (cursor) -> - loc = cursor_to_xyh cursor[0], cursor[1] - unless loc? - console.log "error: tried to move cursor to position that has no pixel location", cursor[0], cursor[1] - return @cursor = cursor # replace cursor element, to reset blink animation if @cursor_visible @@ -891,14 +898,14 @@ class PeachHTML5Editor @cursor_el = domify @outer_idoc, div: id: 'cursor' @overlay.appendChild @cursor_el @cursor_visible = true - @cursor_el.style.left = "#{loc.x + overlay_padding - 1}px" - if loc.h < 5 + @cursor_el.style.left = "#{cursor.x + overlay_padding - 1}px" + if cursor.h < 5 height = 12 else - height = loc.h - @cursor_el.style.top = "#{loc.y + overlay_padding + Math.round(height * .07)}px" + 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[0] + @matt cursor.n matt: (n) -> while @matting.length > 0 @overlay.removeChild @matting[0] -- 1.7.10.4