X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=editor.coffee;h=785615c8b294d22c140dfabd1f48fe25158b1a3d;hb=163adca780b3f37fd3fb1e09cb7bef1dd38c1c84;hp=b6f28f058bf8165f5b77404c779c0d25b1fd1faf;hpb=57b00d36f6efe5e9133ffb4346086c81eb9d89c0;p=peach-html5-editor.git diff --git a/editor.coffee b/editor.coffee index b6f28f0..785615c 100644 --- a/editor.coffee +++ b/editor.coffee @@ -235,99 +235,6 @@ domify = (doc, hash) -> el.setAttribute k, v return el -outer_css = (args) -> - w = args.w ? 300 - h = args.h ? 300 - inner_padding = args.inner_padding ? overlay_padding - frame_width = args.frame_width ? inner_padding - # TODO editor controls height... - occupy = (left, top = left, right = left, bottom = top) -> - w -= left + right - h -= top + bottom - return Math.max(left, top, right, bottom) - ret = '' - ret += 'body {' - ret += 'margin: 0;' - ret += 'padding: 0;' - ret += 'color: black;' - ret += 'background: white;' - ret += '}' - ret += '#wrap1 {' - ret += "border: #{occupy 1}px solid black;" - ret += "padding: #{occupy frame_width}px;" - ret += '}' - ret += '#wrap2 {' - ret += "border: #{occupy 1}px solid black;" - ret += "padding: #{occupy inner_padding}px;" - ret += "padding-right: #{inner_padding + occupy 0, 0, 15, 0}px;" # for scroll bar - ret += "width: #{w}px;" - ret += "height: #{h}px;" - ret += 'overflow-x: hidden;' - ret += 'overflow-y: scroll;' - ret += '}' - ret += '#wrap3 {' - ret += 'position: relative;' - ret += "width: #{w}px;" - ret += "min-height: #{h}px;" - ret += '}' - ret += 'iframe {' - ret += 'box-sizing: border-box;' - ret += 'margin: 0;' - ret += 'border: none;' - ret += 'padding: 0;' - ret += "width: #{w}px;" - #ret += "height: #{h}px;" # height auto-set when content set/changed - ret += '-ms-user-select: none;' - ret += '-webkit-user-select: none;' - ret += '-moz-user-select: none;' - ret += 'user-select: none;' - ret += '}' - ret += '#overlay {' - ret += 'position: absolute;' - ret += "left: -#{inner_padding}px;" - ret += "top: -#{inner_padding}px;" - ret += "right: -#{inner_padding}px;" - ret += "bottom: -#{inner_padding}px;" - ret += 'overflow: hidden;' - ret += '}' - ret += '.lightbox {' - ret += 'position: absolute;' - ret += 'background: rgba(100,100,100,0.2);' - ret += '}' - ret += '#cursor {' - ret += 'position: absolute;' - ret += 'width: 2px;' - ret += 'background: linear-gradient(0deg, rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1));' - ret += 'background-size: 200% 200%;' - ret += '-webkit-animation: blink 1s linear normal infinite;' - ret += 'animation: blink 1s linear normal infinite;' - ret += '}' - ret += '@-webkit-keyframes blink {' - ret += '0%{background-position:0% 0%}' - ret += '100%{background-position:0% -100%}' - ret += '}' - ret += '@keyframes blink { ' - ret += '0%{background-position:0% 0%}' - ret += '100%{background-position:0% -100%}' - ret += '}' - ret += '.ann_box {' - ret += 'z-index: 5;' - ret += 'position: absolute;' - ret += 'border: 1px solid rgba(0,0,0,0.1);' - ret += 'outline: 1px solid rgba(255,255,255,0.1);' # in case there's a black background - ret += '}' - ret += '.ann_tag {' - ret += 'z-index: 10;' - ret += 'position: absolute;' - ret += 'font-size: 8px;' - ret += 'white-space: pre;' - ret += 'background: rgba(255,255,255,0.4);' - ret += '-ms-user-select: none;' - ret += '-webkit-user-select: none;' - ret += '-moz-user-select: none;' - ret += 'user-select: none;' - ret += '}' - return ret ignore_key_codes = @@ -464,6 +371,71 @@ find_prev_cursor_position = (tree, cursor) -> return false return found # maybe null +find_up_cursor_position = (tree, cursor, ideal_x) -> + new_cursor = cursor + # 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 null unless new_cursor? + # done early if we're already left of old cursor position + if new_cursor.x <= ideal_x + return new_cursor + 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 > ideal_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? + if new_cursor.y is target_y + # both valid, and on the same line, use closest + if (ideal_x - new_cursor.x) < (prev_cursor.x - ideal_x) + return new_cursor + else + return prev_cursor + else + # new_cursor on wrong line, use prev_cursor + return prev_cursor + else + # can't go any further prev, use prev_cursor + return prev_cursor + +find_down_cursor_position = (tree, cursor, ideal_x) -> + new_cursor = cursor + # go next until we move on the y axis + while new_cursor.y <= cursor.y + new_cursor = find_next_cursor_position tree, new_cursor + return null unless new_cursor? + # done early if we're already right of old cursor position + if new_cursor.x >= ideal_x + # this would be strange, but could happen due to runaround + return new_cursor + target_y = new_cursor.y + # search rightward, until we find the closest position + # new_cursor is the next-most position we've checked + # prev_cursor is the older value, so it's not as next as new_cursor + while new_cursor.x < ideal_x and new_cursor.y is target_y + prev_cursor = new_cursor + new_cursor = find_next_cursor_position tree, new_cursor + break unless new_cursor? + # move cursor to prev_cursor or new_cursor + if new_cursor? + if new_cursor.y is target_y + # both valid, and on the same line, use closest + if (new_cursor.x - ideal_x) < (ideal_x - prev_cursor.x) + return new_cursor + else + return prev_cursor + else + # new_cursor on wrong line, use prev_cursor + return prev_cursor + else + # can't go any further prev, use prev_cursor + return prev_cursor + xy_to_cursor = (tree, xy) -> for n in tree if n.type is 'tag' or n.type is 'text' @@ -572,25 +544,35 @@ tree_dedup_space = (tree) -> 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 + remove = (undo) -> + if undo + 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 -1 + else + 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 1 + whitespace_to_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 + 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] bounds = text_range_bounds cur.el, cur_i, cur_i + 1 # consistent cases: # 1. zero rects returned by getClientRects() means collapsed space @@ -598,7 +580,7 @@ tree_dedup_space = (tree) -> return remove() # 2. width greater than zero means visible space if bounds.w > 0 - return false + fixers.shift() # don't try removing # now the weird edge cases... # # firefox and chromium both report zero width for characters at the end @@ -613,22 +595,38 @@ tree_dedup_space = (tree) -> 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 = 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 = 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 - # console.log "removing space becase space after it is collapsed" - return true + undo_arg = true # just for readabality + removed = 0 + for fixer in fixers + break if removed > 0 + removed += fixer() + need_undo = false + if prev? + if prev_px? + new_prev_px = new_cursor_position n: prev, i: prev_i + if new_prev_px? + if new_prev_px.x isnt prev_px.x or new_prev_px.y isnt prev_px.y + need_undo = true + else + need_undo = true + else + console.log "this shouldn't happen, we remove spaces that don't locate" + if next? and not need_undo + if next_px? + new_next_px = new_cursor_position n: next, i: next_i + if new_next_px? + if new_next_px.x isnt next_px.x or new_next_px.y isnt next_px.y + need_undo = true + else + need_undo = true + #else + # console.log "removing space becase space after it is collapsed" + if need_undo + removed += fixer undo_arg + if removed > 0 + return true + else + return false # pass null at start/end of display:block queue = (n, i) -> next = n @@ -672,14 +670,15 @@ class PeachHTML5Editor @outer_iframe # iframe to hold editor @outer_idoc # "document" object for @outer_iframe @wrap2 = null # scrollbar is on this + @wrap2_offset = null + @wrap2_height = null # including padding @iframe = null # iframe to hold editable content @idoc = null # "document" object for @iframe @cursor = null @cursor_el = null @cursor_visible = false + @cursor_ideal_x = null @poll_for_blur_timeout = null - @wrap2_offset = null - @iframe_height = null opt_fragment = @options.fragment ? true @parser_opts = {} if opt_fragment @@ -720,7 +719,7 @@ class PeachHTML5Editor outer_bounds.h = 300 outer_iframe_style += "width: #{outer_bounds.w}px; height: #{outer_bounds.h}px;" @outer_iframe.setAttribute 'style', outer_iframe_style - css = outer_css w: outer_bounds.w, h: outer_bounds.h + css = @generate_outer_css w: outer_bounds.w, h: outer_bounds.h outer_wrap.appendChild @outer_iframe init_1: -> # @iframe has loaded (but not it's css) @idoc = @iframe.contentDocument @@ -757,6 +756,99 @@ class PeachHTML5Editor @load_html @in_el.value if @options.on_init? @options.on_init() + generate_outer_css: (args) -> + w = args.w ? 300 + h = args.h ? 300 + inner_padding = args.inner_padding ? overlay_padding + frame_width = args.frame_width ? inner_padding + occupy = (left, top = left, right = left, bottom = top) -> + w -= left + right + h -= top + bottom + return Math.max(left, top, right, bottom) + ret = '' + ret += 'body {' + ret += 'margin: 0;' + ret += 'padding: 0;' + ret += 'color: black;' + ret += 'background: white;' + ret += '}' + ret += '#wrap1 {' + ret += "border: #{occupy 1}px solid black;" + ret += "padding: #{occupy frame_width}px;" + ret += '}' + ret += '#wrap2 {' + ret += "border: #{occupy 1}px solid black;" + @wrap2_height = h # including padding because padding scrolls + ret += "padding: #{occupy inner_padding}px;" + ret += "padding-right: #{inner_padding + occupy 0, 0, 15, 0}px;" # for scroll bar + ret += "width: #{w}px;" + ret += "height: #{h}px;" + ret += 'overflow-x: hidden;' + ret += 'overflow-y: scroll;' + ret += '}' + ret += '#wrap3 {' + ret += 'position: relative;' + ret += "width: #{w}px;" + ret += "min-height: #{h}px;" + ret += '}' + ret += 'iframe {' + ret += 'box-sizing: border-box;' + ret += 'margin: 0;' + ret += 'border: none;' + ret += 'padding: 0;' + ret += "width: #{w}px;" + #ret += "height: #{h}px;" # height auto-set when content set/changed + ret += '-ms-user-select: none;' + ret += '-webkit-user-select: none;' + ret += '-moz-user-select: none;' + ret += 'user-select: none;' + ret += '}' + ret += '#overlay {' + ret += 'position: absolute;' + ret += "left: -#{inner_padding}px;" + ret += "top: -#{inner_padding}px;" + ret += "right: -#{inner_padding}px;" + ret += "bottom: -#{inner_padding}px;" + ret += 'overflow: hidden;' + ret += '}' + ret += '.lightbox {' + ret += 'position: absolute;' + ret += 'background: rgba(100,100,100,0.2);' + ret += '}' + ret += '#cursor {' + ret += 'position: absolute;' + ret += 'width: 2px;' + ret += 'background: linear-gradient(0deg, rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1), rgba(255,255,255,1), rgba(0,0,0,1));' + ret += 'background-size: 200% 200%;' + ret += '-webkit-animation: blink 1s linear normal infinite;' + ret += 'animation: blink 1s linear normal infinite;' + ret += '}' + ret += '@-webkit-keyframes blink {' + ret += '0%{background-position:0% 0%}' + ret += '100%{background-position:0% -100%}' + ret += '}' + ret += '@keyframes blink { ' + ret += '0%{background-position:0% 0%}' + ret += '100%{background-position:0% -100%}' + ret += '}' + ret += '.ann_box {' + ret += 'z-index: 5;' + ret += 'position: absolute;' + ret += 'border: 1px solid rgba(0,0,0,0.1);' + ret += 'outline: 1px solid rgba(255,255,255,0.1);' # in case there's a black background + ret += '}' + ret += '.ann_tag {' + ret += 'z-index: 10;' + ret += 'position: absolute;' + ret += 'font-size: 8px;' + ret += 'white-space: pre;' + ret += 'background: rgba(255,255,255,0.4);' + ret += '-ms-user-select: none;' + ret += '-webkit-user-select: none;' + ret += '-moz-user-select: none;' + ret += 'user-select: none;' + ret += '}' + return ret overlay_event_to_inner_xy: (e) -> unless @wrap2_offset? @wrap2_offset = get_el_bounds @wrap2 @@ -800,37 +892,11 @@ class PeachHTML5Editor return false when KEY_UP if @cursor? - new_cursor = @cursor - # 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? - # 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 + new_cursor = find_up_cursor_position @tree, @cursor, @cursor_ideal_x if new_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 - @move_cursor prev_cursor - else - # 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 + saved_ideal_x = @cursor_ideal_x + @move_cursor new_cursor + @cursor_ideal_x = saved_ideal_x else # move cursor to first position in document new_cursor = first_cursor_position @tree @@ -839,38 +905,11 @@ class PeachHTML5Editor return false when KEY_DOWN if @cursor? - new_cursor = @cursor - # go next until we move on the y axis - while new_cursor.y <= @cursor.y - new_cursor = find_next_cursor_position @tree, new_cursor - return false unless new_cursor? - # done early if we're already right of old cursor position - if new_cursor.x >= @cursor.x - # this would be strange, but could happen due to runaround - @move_cursor new_cursor - return false - target_y = new_cursor.y - # search rightward, until we find the closest position - # new_cursor is the next-most position we've checked - # prev_cursor is the older value, so it's not as next as new_cursor - while new_cursor.x < @cursor.x and new_cursor.y is target_y - prev_cursor = new_cursor - new_cursor = find_next_cursor_position @tree, new_cursor - break unless new_cursor? - # move cursor to prev_cursor or new_cursor + new_cursor = find_down_cursor_position @tree, @cursor, @cursor_ideal_x if new_cursor? - if new_cursor.y is target_y - # both valid, and on the same line, use closest - if (new_cursor.x - @cursor.x) < (@cursor.x - prev_cursor.x) - @move_cursor new_cursor - else - @move_cursor prev_cursor - else - # 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 + saved_ideal_x = @cursor_ideal_x + @move_cursor new_cursor + @cursor_ideal_x = saved_ideal_x else # move cursor to first position in document new_cursor = last_cursor_position @tree @@ -878,6 +917,9 @@ class PeachHTML5Editor @move_cursor new_cursor return false when KEY_END + new_cursor = last_cursor_position @tree + if new_cursor? + @move_cursor new_cursor return false when KEY_BACKSPACE return false unless @cursor? @@ -908,6 +950,9 @@ class PeachHTML5Editor when KEY_ESCAPE return false when KEY_HOME + new_cursor = first_cursor_position @tree + if new_cursor? + @move_cursor new_cursor return false when KEY_INSERT return false @@ -951,13 +996,14 @@ class PeachHTML5Editor @load_html @in_el.value @adjust_iframe_height() adjust_iframe_height: -> + s = @wrap2.scrollTop + # when the content gets shorter, the idoc's body tag will continue to + # report the old (too big) height in Chrome. The workaround is to + # shrink the iframe before the content height: + @iframe.style.height = "10px" h = parseInt(@idoc.body.scrollHeight, 10) - if @iframe_height isnt h - @iframe_height = h - s = @wrap2.scrollTop - @iframe.style.height = "0" - @iframe.style.height = "#{h}px" - @wrap2.scrollTop = s + @iframe.style.height = "#{h}px" + @wrap2.scrollTop = s # does this node have whitespace that would be collapsed by white-space: normal? # note: this checks direct text children, and does _not_ recurse into child tags # tag is a node with type:"tag" @@ -1046,6 +1092,7 @@ class PeachHTML5Editor @cursor = null @annotate null move_cursor: (cursor) -> + @cursor_ideal_x = cursor.x @cursor = cursor unless @cursor_visible @cursor_el = domify @outer_idoc, div: id: 'cursor' @@ -1059,6 +1106,35 @@ class PeachHTML5Editor @cursor_el.style.top = "#{cursor.y + overlay_padding + Math.round(height * .07)}px" @cursor_el.style.height = "#{Math.round height * 0.82}px" @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 + @wrap2.scrollTop = 0 + return + # very bottom of document + if y + h >= @wrap2.scrollHeight - closest + @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 = Math.max(upmost, 0) + # the most scrolled down (highest value for scrollTop) that would be OK + downmost = y - closest + downmost = Math.min(downmost, @wrap2.scrollHeight - @wrap2_height) + if upmost > downmost # means h is too big to fit + # scroll so top is visible + @wrap2.scrollTop = downmost + return + if @wrap2.scrollTop < upmost + @wrap2.scrollTop = upmost + return + if @wrap2.scrollTop > downmost + @wrap2.scrollTop = downmost + return + return annotate: (n) -> while @matting.length > 0 @overlay.removeChild @matting[0]