h: rect.height ? (rect.top - rect.bottom)
}
+is_display_block = (el) ->
+ if el.currentStyle?
+ return el.currentStyle.display is 'block'
+ else
+ return window.getComputedStyle(el, null).getPropertyValue('display') is 'block'
+
# Warning: currently assumes you're asking about a single character
# Note: chromium returns multiple bounding rects for a space at a line-break
# Note: chromium's getBoundingClientRect() is broken (when zero-area client rects)
is_space = (chr) ->
return is_space_code chr.charCodeAt 0
-# warning: contains browser-specific hackery
-is_space_significant = (n, i) ->
- range = document.createRange()
- range.setStart n.el, i
- range.setEnd n.el, i + 1
- rects = range.getClientRects()
- bounding_rect = range.getBoundingClientRect()
- if rects.length is 0
- return false
- if rects.length > 1
- # chromium returns two rects in both these cases:
- # 1. a space that is word-wrapped. one rect on each line. Note that
- # chromium does _not_ do this for _all_ spaces that are word wrapped.
- # 2. the last (insignificant) space in a sequence of collapsing spaces
- # in this case the rects are identical.
- if rects[1].top > rects[0].top
- return true
- width = rects[0].width ? (rects[0].right - rects[0].left)
- if width > 0
- return true
- # firefox reports the space that's word-wrapped as zero width
- if n.text.length > i + 1
- range.setStart n.el, i + 1
- range.setEnd n.el, i + 2
- next_rects = range.getClientRects()
- if next_rects.length > 0
- if next_rects[0].top > rects[0].top
- # next character is lower on the screen, so this must be a word-wrap space
- return true
- else
- # FIXME detect word-wrap in last character
- # could be followed by an inline block with no starting space
- # FIXME chromium gets here for a significant space at the begining of a
- # text node that word-wraps
- return false
+# pass a array of nodes (from parser library, ie it should have .el and .text)
+tree_dedup_space = (tree) ->
+ prev = cur = next = null
+ prev_i = cur_i = next_i = 0
+ prev_pos = pos = next_pos = null
+ prev_px = cur_px = next_px = null
+ first = true
+ removed_char = null
-# pass a node (from parser library, ie it should have .el and .text)
-remove_insignificant_whitespace = (n) ->
- changed = false
- if n.type is TYPE_TEXT
- i = 0
- while i < n.text.length
- if is_space_code n.text.charCodeAt i
- if is_space_significant n, i
- i += 1
- else
- n.el.textContent = n.text = (n.text.substr 0, i) + (n.text.substr i + 1)
- changed = true
+ iterate = (tree, cb) ->
+ for n in tree
+ if n.type is TYPE_TEXT
+ i = 0
+ while i < n.text.length # don't foreach, cb might remove chars
+ removed = cb n, i
+ unless removed
+ i += 1
+ if n.type is TYPE_TAG
+ block = is_display_block n.el
+ if block
+ cb null
+ if n.children.length > 0
+ iterate n.children, cb
+ if block
+ cb null
+ # remove cur char
+ remove = ->
+ removed_char = cur.text.charAt(cur_i)
+ cur.el.textContent = cur.text = (cur.text.substr 0, cur_i) + (cur.text.substr cur_i + 1)
+ if next is cur # in same text node
+ if next_i is 0
+ throw "how is this possible?"
+ next_i -= 1
+ return true
+ # undo remove()
+ put_it_back = ->
+ cur.el.textContent = cur.text = (cur.text.substr 0, cur_i) + removed_char + (cur.text.substr cur_i)
+ if next is cur # in same text node
+ next_i += 1
+ return false
+ # 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
+ bounds = text_range_bounds cur.el, cur_i, cur_i + 1
+ # consistent cases:
+ # 1. zero rects returned by getClientRects() means collapsed space
+ if bounds is null
+ return remove()
+ # 2. width greater than zero means visible space
+ if bounds.w > 0
+ return false
+ # now the weird edge cases...
+ #
+ # firefox and chromium both report zero width for characters at the end
+ # of a line where the text wraps (automatically, due to word-wrap) to
+ # the next line. These do not appear to be distinguishable from
+ # collapsed spaces via the range/bounds api, so...
+ #
+ # 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
+ if next? and not next_px?
+ next_px = cursor_to_xyh next, 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
+ if new_prev_px.x isnt prev_px.x or new_prev_px.y isnt prev_px.y
+ return put_it_back()
else
- i += 1
- if n.children.length > 0
- for c in n.children
- if remove_insignificant_whitespace c
- changed = true
- return changed
+ 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
+ if new_next_px.x isnt next_px.x or new_next_px.y isnt next_px.y
+ return put_it_back()
+ #else
+ # console.log "removing space becase space after it is collapsed"
+ # if there's no prev or next (single space inside a block-level element?) check
+ # TODO scrapt this, or fix it so it works when there's no parent
+ # if prev is null and next is null
+ # new_parent_px = cur.parent.el.getBoundingClientRect()
+ # if new_parent_px.left isnt parent_px.left or new_parent_px.top isnt parent_px.top or new_parent_px.right isnt parent_px.right or new_parent_px.bottom isnt parent_px.bottom
+ # console.log "WEIRD: parent moved"
+ # return put_it_back()
+ # we didn't put it back
+ return true
+ # pass null at start/end of display:block
+ queue = (n, i) ->
+ next = n
+ next_i = i
+ next_px = null
+ if cur?
+ removed = operate()
+ else
+ removed = false
+ unless removed
+ prev = cur
+ prev_i = cur_i
+ prev_px = cur_px
+ cur = next
+ cur_i = next_i
+ cur_px = next_px
+ return removed
+ queue null
+ iterate tree, queue
+ queue null
class PeachHTML5Editor
constructor: (in_el, options = {}) ->
@tree = peach_parser.parse html, @parser_opts
@clear_dom()
instantiate_tree @tree, @idoc.body
- remove_insignificant_whitespace type: TYPE_TAG, children: @tree
+ tree_dedup_space @tree
@changed()
changed: ->
# FIXME don't export cursor placeholder (when cursor is between space characters)