Skip to content

Instantly share code, notes, and snippets.

@axiixc
Created August 18, 2013 08:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save axiixc/6260503 to your computer and use it in GitHub Desktop.
Save axiixc/6260503 to your computer and use it in GitHub Desktop.
Just a little something I'm working on…
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html {
height: 100%;
overflow-y: hidden; }
body {
background: #dfdfdf;
height: 100%;
margin: 0;
padding: 0;
overflow-y: scroll; }
#editor {
background: rgb(251, 251, 251);
max-width: 800px;
min-height: 75%;
margin: 22px auto 0 auto;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
overflow-x: hidden;
outline: none;
pointer-events: auto; }
#editor * {
font-family: "CMU Serif", Georgia, Palatino, Times, 'Times New Roman', serif; }
#editor-title {
font-size: 20pt;
padding: 10px 20px;
width: 100%;
outline: none;
border: none;
background: transparent; }
#editor-divider {
background: linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0) 100%);
height: 1px; }
#editor-body {
white-space: pre-wrap;
outline: none; }
#editor-body p, div {
margin: 20px; }
</style>
<script src="editor.js"></script>
<script>
window.addEventListener('load', function() {
new AXEditor({
titleElement: document.querySelector('#editor-title'),
editorElement: document.querySelector('#editor-body'),
autofocusElement: document.querySelector('#editor'),
delegate:
{
selectionFormatChanged: function(newFormat) { console.log('selectionForamt =', newFormat) },
getPrimaryElement: function() { return document.querySelector('#editor-body') }
}
})
})
</script>
</head>
<body>
<div id="editor">
<input id="editor-title">
<div id="editor-divider"></div>
<div id="editor-body" contenteditable></div>
</div>
</body>
</html>
AXEditor = function(initOptions)
{
this.titleElement = initOptions.titleElement
console.assert(!this.titleElement || this.titleElement instanceof HTMLInputElement, 'titleElement must be of type <input>')
this.editorElement = initOptions.editorElement
console.assert(this.editorElement instanceof HTMLElement, 'editorElement must be an HTML element')
this.autofocusElement = initOptions.autofocusElement
this.delegate = initOptions.delegate || {
getPrimaryElement: function() { return initOptions.editorElement }
}
this.init()
}
AXEditor.PlaceholderModeClassName = 'ax-placeholder-mode'
AXEditor.SelectionFormatNone = 'None'
AXEditor.SelectionFormatPlainText = 'Plain'
AXEditor.SelectionFormatRichText = 'Rich'
AXEditor.prototype =
{
init: function()
{
new AXEditorElementController(this.editorElement)
if (this.delegate.selectionFormatChanged)
{
var selectionFormatChangedToNone = (function() { this.delegate.selectionFormatChanged(AXEditor.SelectionFormatNone) }).bind(this),
selectionFormatChangedToPlainText = (function() { this.delegate.selectionFormatChanged(AXEditor.SelectionFormatPlainText) }).bind(this),
selectionFormatChangedToRichText = (function() { this.delegate.selectionFormatChanged(AXEditor.SelectionFormatRichText) }).bind(this)
this.editorElement.addEventListener('focus', selectionFormatChangedToRichText)
this.editorElement.addEventListener('blur', selectionFormatChangedToNone)
this.titleElement.addEventListener('focus', selectionFormatChangedToPlainText())
this.titleElement.addEventListener('blur', selectionFormatChangedToNone())
}
if (this.autofocusElement && this.autofocusElement.autofocusTarget)
console.error('Element supplied as autofocus element already has a target, failed to rebind')
else if (this.autofocusElement && this.delegate.getPrimaryElement)
{
this.autofocusElement.autofocusTarget = this
if (!document.documentElement.ontouchstart)
{
this.autofocusElement.addEventListener('click', function(event) {
if (this === event.target)
this.autofocusTarget.focusPrimaryElement(true)
})
}
else
{
var shouldAutofocus = false
this.autofocusElement.addEventListener('touchstart', function(event) { shouldAutofocus = (this === event.target) })
this.autofocusElement.addEventListener('touchmove', function(event) { shouldAutofocus = false })
this.autofocusElement.addEventListener('touchcancel', function(event) { shouldAutofocus = false })
this.autofocusElement.addEventListener('touchend', function(event) {
if (!shouldAutofocus) return
shouldAutofocus = false
this.autofocusTarget.focusPrimaryElement(true)
})
}
}
},
focusPrimaryElement: function(focusToEnd)
{
var primaryElement = this.delegate.getPrimaryElement()
if (!primaryElement) return
if (!focusToEnd || primaryElement.classList.contains(AXEditor.PlaceholderModeClassName))
primaryElement.focus()
else
{
if (primaryElement.setSelectionRange)
{
var length = primaryElement.value.length
primaryElement.setSelectionRange(length, length)
return
}
var range = document.createRange()
range.selectNodeContents(primaryElement)
range.collapse(false)
var selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
}
},
saveSelection: function()
{
this.savedRange = window.getSelection().getRangeAt(0)
},
restoreSelection: function()
{
if (!this.savedRange) return
var selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(this.savedRange)
this.ensureCaretVisible()
delete this.savedRange
}
}
AXEditorElementController = function(targetElement)
{
targetElement.classList.add(AXEditor.PlaceholderModeClassName)
if (targetElement.placeholder) targetElement.innerHTML = targetElement.placeholder
targetElement.addEventListener('focus', this.elementDidFocus)
targetElement.addEventListener('blur', this.elementDidBlur)
var needsCaretRepositionFix = (/(iPad|iPhone|iPod)/).test(navigator.userAgent)
if (needsCaretRepositionFix)
{
targetElement.addEventListener('keyup', this.keyupListener.bind(this))
targetElement.addEventListener('keydown', this.keydownListener.bind(this))
}
}
AXEditorElementController.DeferedUpdateKeyCount = 50
AXEditorElementController.DeferedUpdateDuration = 500
AXEditorElementController.prototype =
{
keyupListener: function()
{
if (++this.keyPressCount > AXEditorElementController.DeferedUpdateKeyCount)
{
this.keyPressCount = 0
this.updateIfNeeded()
}
else
{
clearTimeout(this.timeout)
this.timeout = setTimeout(this.updateIfNeeded, AXEditorElementController.DeferedUpdateDuration)
}
},
keydownListener: function(event)
{
// Current State:
// - Detects blockquotes, but removal isn't right
// - Needs to un-quote when at the BEGINNING OF A LINE even deep wthin the body
if (event.keyCode !== 8)
return
var selectionAction = getSelection().adjustedDeleteKeyAction(this.targetElement)
if (selectionAction !== Selection.AdjustedDeleteKeyActionNothing)
{
event.preventDefault()
event.stopPropagation()
}
if (selectionAction === Selection.AdjustedDeleteKeyActionUnquote)
AXSelectionFormatting.decreaseQuoteLevel()
},
ensureCaretVisible: function()
{
const ContentInsertTop = 20
var selectionCoordinates = getSelection().getCoordinates(true)
if (selectionCoordinates.y > window.innerHeight - ContentInsertTop)
return scrollTo(window.pageXOffset, (selectionCoordinates.y - window.innerHeight) + ContentInsertTop + ContentInsertTop + window.pageYOffset)
if (selectionCoordinates.y < ContentInsertTop)
return scrollTo(window.pageXOffset, window.pageYOffset - ContentInsertTop - ContentInsertTop + selectionCoordinates.y)
},
elementDidFocus: function(event)
{
if (!this.classList.contains(AXEditor.PlaceholderModeClassName)) return
while (this.firstChild)
this.removeChild(this.firstChild)
var p = document.createElement('p')
p.appendChild(document.createElement('br'))
this.appendChild(p)
var range = document.createRange()
range.selectNodeContents(p)
range.collapse()
var sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
this.classList.remove(AXEditor.PlaceholderModeClassName)
},
elementDidBlur: function(event)
{
if (!this.isEmpty()) return
this.classList.add(AXEditor.PlaceholderModeClassName)
if (this.placeholder) this.innerHTML = this.placeholder
}
}
AXSelectionFormatting =
{
removeLink: function()
{
var selection = getSelection()
if (selection.isCollapsed)
return document.execCommand('unlink', false, null)
var node = selection.getAnchorElement(),
lastSelection = selection.getRangeAt(0)
var range = document.createRange()
range.selectNodeContents(node)
selection.removeAllRanges()
selection.addRange(range)
document.execCommand('unlink', false, null)
selection.removeAllRanges()
selection.addRange(lastSelection)
},
createLink: function(url, text)
{
if (!text)
return document.execCommand('createLink', false, url)
var link = document.createElement('a')
link.href = url
link.innerText = text
this.insertHTML(link.outerHTML)
},
insertHTML: function(html) { document.execCommand('insertHTML', false, html) },
insertText: function(text) { document.execCommand('insertText', false, text) },
insertImage: function(imgUrl) { document.execCommand('insertImage', false, imgUrl) },
bold: function() { document.execCommand('bold', false, null) },
italic: function() { document.execCommand('italic', false, null) },
underline: function() { document.execCommand('underline', false, null) },
strike: function() { document.execCommand('strikeThrough', false, null) },
increaseQuoteLevel: function() { document.execCommand('indent', false, null) },
decreaseQuoteLevel: function() { document.execCommand('outdent', false, null) }
}
Element.prototype.isEmpty = function(elem)
{
var noImage = !this.querySelector('img')
var noText = !(/\S/.test(this.innerText))
return noImage && noText
}
Selection.AdjustedDeleteKeyActionNothing = 0
Selection.AdjustedDeleteKeyActionUnquote = 1
Selection.AdjustedDeleteKeyActionOutdent = 2
Selection.prototype.adjustedDeleteKeyAction = function(topLevelContainer)
{
var range = this.getRangeAt(0)
// If we aren't at the start of the range, we can assume we aren't at the start of anything important
if (range.startOffset !== 0)
return Selection.AdjustedDeleteKeyActionNothing
// // Take a reference to the editor, this is the stop condition
// var contentsElem = $('#ff-editor-contents');
//
// var me = $(range.startContainer);
//
// // -- IF I AM FIRST OF PARENT OF FIRST OF PARENT...
// while (!me.is(contentsElem))
// {
// // If I am not the first element of my parent, do nothing.
// if (!me.is(me.parent().children(':first'))) {
// return 'nothing';
// }
//
// // If I am a blockquote, unquote
// if (me.is('blockquote')) {
// console.log("FLAG");
// // return 'blockquote';
// }
//
// me = me.parent();
// }
//
return Selection.AdjustedDeleteKeyActionNothing
}
Selection.prototype.getAnchorElement = function()
{
var node = this.anchorNode
return (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : node
}
Selection.prototype.getHREF = function()
{
return this.getAnchorElement().getAttribute('href')
}
Selection.prototype.getCoordinates = function(start)
{
if (!this.rangeCount) return
var range = this.getRangeAt(this.rangeCount - 1)
range.collapse(start)
var dummyElement = document.createElement('span')
range.insertNode(dummyElement)
var rect = dummy.getBoundingClientRect()
dummyElement.parentNode.removeChild(dummyElement)
return { x: rect.left, y: rect.top }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment