Skip to content

Instantly share code, notes, and snippets.

@sbp
Created July 30, 2012 22:58
Show Gist options
  • Save sbp/3211560 to your computer and use it in GitHub Desktop.
Save sbp/3211560 to your computer and use it in GitHub Desktop.
Edit a DOM using UI controls with contenteditable
node_name = (node) ->
return node.nodeName.toLowerCase()
current_range = () ->
if window.getSelection
selection = window.getSelection()
if selection.getRangeAt and selection.rangeCount
return selection.getRangeAt 0
set_range = (range) ->
selection = window.getSelection()
selection.removeAllRanges()
selection.addRange range
# refreshed = document.createRange()
# refreshed.setStart range.startContainer, range.startOffset
# refreshed.collapse true
UNUSED_clone_range = (range) ->
clone = document.createRange()
clone.setStart $(range.startContainer)[0], range.startOffset
clone.collapse true
return clone
current_node = () ->
range = current_range()
return range.startContainer
current_element = () ->
node = current_node()
if node.parentElement
return node.parentElement
# otherwise, loop to find node.nodeType == 1?
current_tree = () ->
tree = []
node = current_node()
while node.parentElement
node = node.parentElement
tree.push node
return tree
current_tree_names = () ->
names = []
tree = current_tree()
for node in tree
name = node_name node
names.push name
return names
current_block = () ->
blocks =
p: true
h1: true
h2: true
h3: true
h4: true
tree = current_tree()
for node in tree
name = node_name node
if name of blocks
return node
child_index = (node) ->
index = 0
for child in node.parentNode.childNodes
if child is node
return index
index++
current_indices = () ->
indices = []
node = current_node()
block = current_block()
while node != block
index = child_index node
indices.push index
node = node.parentNode
return indices
transform_current_block = (name) ->
indices = current_indices()
offset = current_range().startOffset
block = $ current_block()
dummy = $ "<span></span>"
block.append dummy
block.wrap "<#{name}/>"
transformation = block.parent()
dummy.unwrap()
dummy.remove()
container = transformation[0]
while indices.length
index = indices.pop()
container = container.childNodes[index]
range = document.createRange()
range.setStart container, offset
range.collapse true
set_range range
transform_to_heading = () ->
headings =
h1: "h2"
h2: "h3"
h3: "h4"
h4: "h1"
block_name = node_name current_block()
if block_name of headings
name = headings[block_name]
else name = "h1"
transform_current_block name
transform_to_paragraph = () ->
block_name = node_name current_block()
if block_name isnt "p"
transform_current_block "p"
UNUSED_split_text_node = (node, offset) ->
node = $ node
text = node.text()
a = text.substring 0, offset
b = text.substring offset, text.length
node.text a
b_node = $ ""
b_node.insertAfter node
b_node.text b
return [node, b_node]
insert_into_text = (text, offset, node) ->
text = $ text
text_content = text.text()
# if offset is 0
# return text.before node
# if offset is text_content.length
# return text.after node
before = text_content.substring 0, offset
after = text_content.substring offset
form = $ "<form>&nbsp;</form>"
form.css "display": "inline"
input = $ "<input/>"
input.css
"border": "none"
"border-bottom": "1px solid #ccc"
form.submit ->
node.attr "href", input.val()
form.remove()
caret = document.createRange()
children = node[0].childNodes
end = children[children.length - 1]
caret.setStart end, end.length
caret.collapse
set_range caret
adjacent_shim()
return false
form.append input
text.after before, node, form, after
text.remove()
input.focus()
apply_highlight = () ->
element = $ current_element()
if (element.attr "data-style-backup")
if (element.attr "data-style-backup") isnt ""
return
style = element.attr "style"
console.log style
if style
console.log "backing up style"
element.attr "data-style-backup", element.attr "style"
element.css
"background-color": "rgba(128, 192, 0, 0.25)"
"outline": "1px solid #9c0"
"outline-offset": "0"
editing = () ->
return ($("body").attr "contenteditable") is "true"
remove_highlight = () ->
element = $ current_element()
if (element.attr "data-style-backup")
if (element.attr "data-style-backup") isnt ""
element.attr "style", element.attr "data-style-backup"
element.removeAttr "data-style-backup"
return
element.removeAttr "style"
# element.css
# "background-color": "inherit"
# "outline": "none"
# "outline-offset": "0"
transform_to_anchor = () ->
range = current_range()
contents = range.extractContents()
anchor = $ "<a href=\"#\"></a>"
insert_into_text range.startContainer, range.startOffset, anchor
anchor.append contents
if false
console.log "not contents"
arange = document.createRange()
arange.setStart anchor, 0
arange.collapse true
set_range arange
create_status_bar = () ->
status_bar = $("<div>—</div>")
status_bar.attr "id", "statusbar"
status_bar.attr "contenteditable", "false"
status_bar.css
"position": "fixed"
"bottom": "12px"
"right": "12px"
"padding": "3px"
"background-color": "rgba(192, 192, 192, 0.25)"
"border-radius": "3px"
"box-shadow": "0 0 12px rgba(192, 192, 192, 0.25)"
"min-width": "180px"
"text-align": "right"
$("body").append status_bar
update_status_bar = () ->
if not current_range()
return
node = current_node()
names = []
number = 0
while node.parentElement and (number < 3)
if node.nodeType is 1
names.push node_name node
number++
node = node.parentElement
$("#statusbar").text "editing: "
while names.length
$("#statusbar").append names.pop()
if names.length
$("#statusbar").append " > "
remove_status_bar = () ->
$("#statusbar").remove()
window.editing_element = null
UNUSED_inline_shim = () ->
inline =
a: true
element = current_element()
element_name = node_name element
if element isnt window.editing_element
if element_name of inline
e = $ element
e.prepend "["
e.append "]"
range = current_range()
r = document.createRange()
r.setStart range.startContainer, range.startOffset - 1 # um, or +1
r.collapse true
set_range r
if window.editing_element
if (node_name window.editing_element) of inline
e = $ editing_element
e.css "background-color": "green"
window.editing_element = element
window.adjacentOld = []
window.adjacentNew = []
adjacent = (node) ->
window.adjacentNew.push node
add_anchor_shim = (node) ->
open = document.createTextNode "["
close = document.createTextNode "]"
node = $ node
node.prepend open
node.append close
without = (array, element) ->
index = $.inArray element, array
delete array[index]
return $.map array, (f) -> f
contains = (array, element) ->
return ($.inArray element, array) > -1
remove_anchor_shim = (node) ->
window.adjacentNew = without window.adjacentNew, node
for child in node.childNodes
continue if not child
continue if not child.textContent
if (child.textContent is "[") or (child.textContent is "]")
$(child).remove()
update_adjacent = (node) ->
# Old AND New
console.log "window.adjacentNew:", window.adjacentNew
for ne in window.adjacentNew
if contains window.adjacentOld, ne
console.log "Old AND New:", ne.textContent
window.adjacentOld = without window.adjacentOld, ne
# New - Old
else
add_anchor_shim ne
console.log "New - Old:", ne.textContent
# Old - New
for old in window.adjacentOld
remove_anchor_shim old
console.log "Old - New:", old.textContent
window.adjacentOld = window.adjacentNew
window.adjacentNew = []
adjacent_shim = () ->
range = current_range()
node = current_node()
element = current_element()
console.log range.startOffset, range.startContainer.textContent
if range.startOffset is 0
console.log "Start of node:", node_name element
sibling = node.previousSibling
if sibling
if (node_name sibling) is "a"
console.log "Adjacent:", sibling.textContent
adjacent sibling
if range.startOffset is node.length
console.log "End of node:", node_name element
if (node_name element) is "a"
console.log "Adjacent:", element.textContent
adjacent element
console.log "Current:", element.childNodes[element.childNodes.length - 1]
if element.childNodes[element.childNodes.length - 1] is node
sibling = element.nextSibling
else sibling = node.nextSibling
else sibling = node.nextSibling
if sibling
if (node_name sibling) is "a"
console.log "Adjacent:", sibling.textContent
adjacent sibling
else
if (node_name element) is "a"
adjacent element
# else window.adjacentNew = window.adjacentOld
console.log "Updating adjacent"
update_adjacent()
$ ->
$("body").keydown (e) ->
if e.ctrlKey and e.keyCode != 17
console.log e.keyCode
if e.ctrlKey and e.keyCode is 69 # Ctrl+E
if editing()
$("body").attr "contenteditable", "false"
remove_status_bar()
else
$("body").attr "contenteditable", "true"
create_status_bar()
e.preventDefault()
return if not editing()
if e.ctrlKey and e.keyCode is 72 # Ctrl+H
transform_to_heading()
e.preventDefault()
if e.ctrlKey and e.keyCode is 80 # Ctrl+P
transform_to_paragraph()
e.preventDefault()
if e.ctrlKey and e.keyCode is 65 # Ctrl+A
transform_to_anchor()
e.preventDefault()
if e.ctrlKey and e.keyCode is 186 # Ctrl+;
apply_highlight()
e.preventDefault()
$("body").keyup (e) ->
return if not editing()
update_status_bar()
adjacent_shim()
if e.ctrlKey and e.keyCode is 186 # Ctrl+;
remove_highlight()
e.preventDefault()
$("body").mousedown (e) ->
return if not editing()
$("body").mouseup (e) ->
return if not editing()
update_status_bar()
adjacent_shim()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment