X-Git-Url: https://jasonwoof.com/gitweb/?p=peach-html5-editor.git;a=blobdiff_plain;f=editor.coffee;h=8367d09b799835b528942d687b0c141fcff76e42;hp=012079ca19f4af66e1786d443ae0ba2d903f56d1;hb=8e715e94e59e5bd05fea8e073de2750df2be1f8f;hpb=52ea40cffa19eb1229f067975c18508b140df336 diff --git a/editor.coffee b/editor.coffee index 012079c..8367d09 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' @@ -692,20 +664,22 @@ 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 @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 @@ -746,7 +720,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 @@ -783,6 +757,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 @@ -826,37 +893,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 @@ -865,38 +906,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 @@ -904,18 +918,12 @@ class PeachHTML5Editor @move_cursor new_cursor return false when KEY_END - 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 + new_cursor = last_cursor_position @tree if new_cursor? @move_cursor new_cursor - else - @kill_cursor() + return false + when KEY_BACKSPACE + @on_key_backspace e return false when KEY_DELETE return false unless @cursor? @@ -930,10 +938,15 @@ class PeachHTML5Editor @kill_cursor() return false when KEY_ENTER + @on_key_enter e return false when KEY_ESCAPE + @kill_cursor() 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 @@ -959,6 +972,107 @@ class PeachHTML5Editor console.log "ERROR: couldn't find cursor position after insert" @kill_cursor() return false + on_key_enter: (e) -> # enter key pressed + return unless @cursor_visible + cur_block = @cursor.n + loop + if cur_block.type is 'tag' + if is_display_block cur_block.el + break + return unless cur_block.parent? + cur_block = cur_block.parent + # find array to insert new element into + 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 + i += 1 # we want to be after it + if i < pc.length + before = pc[i].el + else + before = null + # TODO if content after cursor + # TODO new block is empty + new_text = new peach_parser.Node 'text', text: ' ' + new_node = new peach_parser.Node 'tag', name: 'p', parent: cur_block.parent, attrs: {style: 'white-space: pre-wrap'}, children: [new_text] + new_text.parent = new_node + new_text.el = domify @idoc, text: ' ' + 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 clear_dom: -> # remove all the editable content (and cursor, overlays, etc) while @idoc.body.childNodes.length @idoc.body.removeChild @idoc.body.childNodes[0] @@ -966,8 +1080,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: -> @@ -1046,15 +1164,42 @@ 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 + 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 else n.text = @@ -1073,6 +1218,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' @@ -1086,6 +1232,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] @@ -1093,7 +1268,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