Created
August 18, 2013 08:05
-
-
Save axiixc/6260503 to your computer and use it in GitHub Desktop.
Just a little something I'm working on…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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