X-Git-Url: https://jasonwoof.com/gitweb/?p=peach-html5-editor.git;a=blobdiff_plain;f=editor.coffee;h=dd39c6223016adc38779af63fb2652aa13fb31fb;hp=9e2154460af8520701db0c3dbc707e688cf7f6c6;hb=30aee05f9c37a3954f5f849823b2e84a304423ff;hpb=3b0ea1cf0431211db79091277e2dec49f2c1bbd3 diff --git a/editor.coffee b/editor.coffee index 9e21544..dd39c62 100644 --- a/editor.coffee +++ b/editor.coffee @@ -16,6 +16,7 @@ # SETTINGS overlay_padding = 10 +breathing_room = 30 # minimum pixels above/below cursor timeout = (ms, cb) -> return setTimeout cb, ms next_frame = (cb) -> @@ -558,21 +559,32 @@ tree_dedup_space = (tree) -> throw "how is this possible?" next_i -= 1 return 1 - whitespace_to_space = (undo) -> + replace_with_space = (undo) -> if undo cur.text = (cur.text.substr 0, cur_i) + removed_char + (cur.text.substr cur_i + 1) cur.el.textContent = cur.text else removed_char = cur.text.charAt(cur_i) - cur.text = (cur.text.substr 0, cur_i) + ' ' + (cur.text.substr cur_i + 1) - cur.el.textContent = cur.text + if removed_char isnt ' ' + cur.text = (cur.text.substr 0, cur_i) + ' ' + (cur.text.substr cur_i + 1) + cur.el.textContent = cur.text return 0 # return true if cur was removed from the dom (ie re-use same prev) operate = -> # cur definitately set # prev and/or next might be null, indicating the start/end of a display:block return false unless is_space_code cur.text.charCodeAt cur_i - fixers = [remove, whitespace_to_space] + fixers = [remove, replace_with_space] + # check for common case: single whitespace surrounded by non-whitespace chars + if prev? and next? + unless (is_space_code prev.text.charCodeAt prev_i) or (is_space_code next.text.charCodeAt next_i) + dbg = cur.text.charCodeAt cur_i + if cur.text.charAt(cur_i) is ' ' # perens required + # single space can't collapse, doesn't need fixin' + return false + else + # tab, newline, etc, can't collapse, but maybe should be replaced + fixers = [replace_with_space] bounds = text_range_bounds cur.el, cur_i, cur_i + 1 # consistent cases: # 1. zero rects returned by getClientRects() means collapsed space @@ -580,7 +592,8 @@ tree_dedup_space = (tree) -> return remove() # 2. width greater than zero means visible space if bounds.w > 0 - fixers.shift() # don't try removing + # has bounds, don't try removing + fixers = [replace_with_space] # now the weird edge cases... # # firefox and chromium both report zero width for characters at the end @@ -589,6 +602,11 @@ tree_dedup_space = (tree) -> # collapsed spaces via the range/bounds api, so... # # remove it from the dom, and if prev or next moves, put it back. + # + # this block (try changing it, put it back if something moves) is also + # used on collapsable whitespace characters besides space. In this case + # the character is replaced with a normal space character instead of + # removed if prev? and not prev_px? prev_px = new_cursor_position n: prev, i: prev_i if next? and not next_px? @@ -664,7 +682,8 @@ class PeachHTML5Editor 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 @@ -922,16 +941,7 @@ class PeachHTML5Editor @move_cursor new_cursor 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 - if new_cursor? - @move_cursor new_cursor - else - @kill_cursor() + @on_key_backspace e return false when KEY_DELETE return false unless @cursor? @@ -959,8 +969,10 @@ class PeachHTML5Editor 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 @@ -990,12 +1002,12 @@ class PeachHTML5Editor return unless cur_block.parent? cur_block = cur_block.parent # find array to insert new element into - if cur_block.parent?.el? - parent_el = cur_block.parent.el - pc = cur_block.parent.children - else + 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 @@ -1013,10 +1025,157 @@ class PeachHTML5Editor 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 @wrap2.scrollTop is 0 + return unless @cursor? + new_cursor = first_cursor_position @tree + if new_cursor? + if new_cursor.n isnt @cursor.n or new_cursor.i isnt @cursor.i + @move_cursor new_cursor + return + 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) -> + lowest_scrollpos = @wrap2.scrollHeight - @wrap2_height + if @wrap2.scrollTop is lowest_scrollpos + return unless @cursor? + new_cursor = last_cursor_position @tree + if new_cursor? + if new_cursor.n isnt @cursor.n or new_cursor.i isnt @cursor.i + @move_cursor new_cursor + return + if @cursor? + screen_y = @cursor.y - @wrap2.scrollTop + scroll_amount = @wrap2_height - breathing_room + @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 + unless @wrap2.scrollTop is 0 + y_min += breathing_room + y_max = @wrap2.scrollTop + @wrap2_height + unless @wrap2.scrollTop is @wrap2.scrollHeight - @wrap2_height # downmost + y_max -= 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 = (cur, target_y) -> + return cur.y + cur.h <= target_y + else + finder = find_down_cursor_position + far_enough = (cur, y_target) -> + return cur.y >= y_target + loop + cur = finder @tree, was, @cursor_ideal_x + break unless cur? + break if far_enough cur, y_target + was = cur + if was is @cursor + was = null + if was? + if was.y + was.h > y_max + was = null + else if was.y < y_min + was = null + if cur? + if cur.y + cur.h > 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 + was.h > 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] @@ -1024,8 +1183,12 @@ class PeachHTML5Editor return load_html: (html) -> @tree = peach_parser.parse html, @parser_opts + if !@tree[0]?.parent + @tree = peach_parser.parse '

', @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: -> @@ -1104,27 +1267,40 @@ class PeachHTML5Editor 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 - # 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 - special_case = false - if n.text is ' ' - if n.parent?.el? - if n.parent.children.length is 1 - if n.parent.children[0] is n - special_case = true - else - special_case = true - if special_case - n.text = char - else - n.text = char + n.text + 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 @@ -1161,21 +1337,20 @@ class PeachHTML5Editor @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 @@ -1195,7 +1370,7 @@ class PeachHTML5Editor 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