app/assets/javascripts/app/lib/base/jquery.contenteditable.js
(function ($) {
/*
# mode: textonly/richtext / disable b/i/u/enter + strip on paste
# pasteOnlyText: true
# maxlength: 123
# multiline: true / disable enter + strip on paste
# placeholder: 'some placeholder'
# imageWidth: absolute (<img style="width: XXpx; height: XXXpx" src="">) || relative (<img style="width: 100%; max-width: XXpx;" src="">)
*/
var pluginName = 'ce',
defaults = {
debug: false,
mode: 'richtext',
multiline: true,
imageWidth: 'absolute',
noImages: false,
allowKey: {
8: true, // backspace
9: true, // tab
16: true, // shift
17: true, // ctrl
18: true, // alt
20: true, // cabslock
37: true, // up
38: true, // right
39: true, // down
40: true, // left
91: true, // cmd left
92: true, // cmd right
224: true, // cmd left
},
extraAllowKey: {
65: true, // a + ctrl - select all
67: true, // c + ctrl - copy
86: true, // v + ctrl - paste
88: true, // x + ctrl - cut
90: true, // z + ctrl - undo
},
richTextFormatKey: {
66: true, // b
73: true, // i
85: true, // u
83: true, // s
},
//maxlength: 20,
};
function Plugin( element, options ) {
this.element = element;
this.$element = $(element)
this.options = $.extend( {}, defaults, options) ;
this._defaults = defaults;
this._name = pluginName;
// take placeholder from markup
if ( !this.options.placeholder && this.$element.data('placeholder') ) {
this.options.placeholder = this.$element.data('placeholder')
}
this.preventInput = false
// handle contenteditable issues
this.browserMagicKey = App.Browser.magicKey()
this.browserHotkeys = App.Browser.hotkeys()
this.init()
}
Plugin.prototype.init = function () {
this.bindEvents()
this.$element.enableObjectResizingShim()
}
Plugin.prototype.bindEvents = function () {
this.$element.on('keydown', this.onKeydown.bind(this))
this.$element.on('paste', this.onPaste.bind(this))
this.$element.on('dragover', this.onDragover.bind(this))
this.$element.on('drop', this.onDrop.bind(this))
}
Plugin.prototype.toggleBlock = function(tag) {
sel = window.getSelection()
node = $(sel.anchorNode)
if (node.is(tag) || node.parent().is(tag) || node.parent().parent().is(tag)) {
document.execCommand('formatBlock', false, 'div')
//document.execCommand('RemoveFormat')
}
else {
document.execCommand('formatBlock', false, tag)
}
}
Plugin.prototype.onKeydown = function (e) {
this.log('keydown', e.keyCode)
if (this.preventInput) {
this.log('preventInput', this.preventInput)
return
}
// strap the return key being pressed
if (e.keyCode === 13) {
// disbale multi line
if (!this.options.multiline) {
e.preventDefault()
return
}
// break <blockquote> after enter on empty line
sel = window.getSelection()
if (sel) {
node = $(sel.anchorNode)
if (node.closest('blockquote').length > 0) {
// Special handling when the line is not wrapped inside of a html element.
if (!node.is('div') && node.parent().is('blockquote') && node.text()) {
e.preventDefault()
document.execCommand('formatBlock', false, 'div')
document.execCommand('insertParagraph')
return
}
if (!e.shiftKey && node && (node.is('blockquote') || (node.parent() && node.parent().is('blockquote')) || !node.text())) {
e.preventDefault()
document.execCommand('insertParagraph')
document.execCommand('outdent')
return
}
}
}
// behavior to enter new line on alt+enter
// on alt + enter not realy newline is fired, to make
// it compat. to other systems, do it here
if (!e.shiftKey && e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
this.paste('<br><br>')
return
}
}
// on zammad magicKey + i/b/u/s
// hotkeys + u -> Toggles the current selection between underlined and not underlined
// hotkeys + b -> Toggles the current selection between bold and non-bold
// hotkeys + i -> Toggles the current selection between italic and non-italic
// hotkeys + v -> Toggles the current selection between strike and non-strike
// hotkeys + f -> Removes the formatting tags from the current selection
// hotkeys + y -> Removes the formatting from while textarea
// hotkeys + z -> Inserts a Horizontal Rule
// hotkeys + l -> Toggles the text selection between an unordered list and a normal block
// hotkeys + k -> Toggles the text selection between an ordered list and a normal block
// hotkeys + o -> Draws a line through the middle of the current selection
// hotkeys + w -> Removes any hyperlink from the current selection
var richtTextControl = false
if (this.browserMagicKey == 'cmd') {
if (!e.altKey && !e.ctrlKey && e.metaKey) {
richtTextControl = true
}
}
else {
if (!e.altKey && e.ctrlKey && !e.metaKey) {
richtTextControl = true
}
}
if (richtTextControl && this.options.richTextFormatKey[ e.keyCode ]) {
e.preventDefault()
if (e.keyCode == 66) {
document.execCommand('bold')
return true
}
if (e.keyCode == 73) {
document.execCommand('italic')
return true
}
if (e.keyCode == 85) {
document.execCommand('underline')
return true
}
if (e.keyCode == 83) {
document.execCommand('strikeThrough')
return true
}
}
var hotkeys = false
if (this.browserHotkeys == 'ctrl+shift') {
if (!e.altKey && e.ctrlKey && !e.metaKey && e.shiftKey) {
hotkeys = true
}
}
else {
if (e.altKey && e.ctrlKey && !e.metaKey) {
hotkeys = true
}
}
if (hotkeys && (this.options.richTextFormatKey[ e.keyCode ]
|| e.keyCode == 49
|| e.keyCode == 50
|| e.keyCode == 51
|| e.keyCode == 66
|| e.keyCode == 70
|| e.keyCode == 90
|| e.keyCode == 70
|| e.keyCode == 73
|| e.keyCode == 75
|| e.keyCode == 76
|| e.keyCode == 85
|| e.keyCode == 83
|| e.keyCode == 88
|| e.keyCode == 90
|| e.keyCode == 89)) {
e.preventDefault()
// disable rich text b/u/i
if ( this.options.mode === 'textonly' ) {
return
}
if (e.keyCode == 49) {
this.toggleBlock('h1')
}
if (e.keyCode == 50) {
this.toggleBlock('h2')
}
if (e.keyCode == 51) {
this.toggleBlock('h3')
}
if (e.keyCode == 66) {
document.execCommand('bold')
}
if (e.keyCode == 70) {
document.execCommand('removeFormat')
}
if (e.keyCode == 73) {
document.execCommand('italic')
}
if (e.keyCode == 75) {
document.execCommand('insertOrderedList')
}
if (e.keyCode == 76) {
document.execCommand('insertUnorderedList')
}
if (e.keyCode == 85) {
document.execCommand('underline')
}
if (e.keyCode == 83) {
document.execCommand('strikeThrough')
}
if (e.keyCode == 88) {
document.execCommand('unlink')
}
if (e.keyCode == 89) {
var cleanHtml = App.Utils.htmlRemoveRichtext(this.$element.html())
this.$element.html(cleanHtml)
}
if (e.keyCode == 90) {
document.execCommand('insertHorizontalRule')
}
this.log('content editable richtext key', e.keyCode)
return true
}
// limit check
if ( !this.allowKey(e) ) {
if ( !this.maxLengthOk(1) ) {
e.preventDefault()
return
}
}
}
Plugin.prototype.getHtmlFromClipboard = function(clipboardData) {
try {
return clipboardData.getData('text/html')
}
catch (e) {
console.log('Sorry, can\'t get html of clipboard because browser is not supporting it.')
return
}
}
Plugin.prototype.getTextFromClipboard = function(clipboardData) {
var text
try {
text = clipboardData.getData('text/plain')
if (!text || text.length === 0) {
text = clipboardData.getData('text')
}
return text
}
catch (e) {
console.log('Sorry, can\'t get text of clipboard because browser is not supporting it.')
return
}
}
Plugin.prototype.getClipboardData = function(e) {
var clipboardData
if (e.clipboardData) { // ie
clipboardData = e.clipboardData
}
else if (window.clipboardData) { // ie
clipboardData = window.clipboardData
}
else if (e.originalEvent.clipboardData) { // other browsers
clipboardData = e.originalEvent.clipboardData
}
else {
throw "No clipboardData support"
}
return clipboardData
}
Plugin.prototype.getClipboardDataImage = function(clipboardData) {
if (!clipboardData.items || !clipboardData.items[0]) {
return
}
return $.grep(clipboardData.items, function(item){
return item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
})[0]
}
Plugin.prototype.onPaste = function (e) {
e.preventDefault()
var clipboardData, clipboardImage, text, htmlRaw, htmlString
this.log('paste')
clipboardData = this.getClipboardData(e)
// look for image only if no HTML with textual content is available.
// E.g. Excel provides images of the spreadsheet along with HTML.
// While some browsers make images available in clipboard as HTML,
// sometimes wrapped in multiple nodes.
htmlRaw = this.getHtmlFromClipboard(clipboardData)
if (!App.Utils.clipboardHtmlIsWithText(htmlRaw)) {
// insert and in case, resize images
clipboardImage = this.getClipboardDataImage(clipboardData)
if (clipboardImage) {
// stop processing if input form does not accept images
if(this.options.noImages) return;
this.log('paste image', clipboardImage)
var imageFile = clipboardImage.getAsFile()
var reader = new FileReader()
reader.onload = $.proxy(function (e) {
var result = e.target.result
var img = document.createElement('img')
img.src = result
maxWidth = 1000
if (this.$element.width() > 1000) {
maxWidth = this.$element.width()
}
scaleFactor = 2
//scaleFactor = 1
//if (window.isRetina && window.isRetina()) {
// scaleFactor = 2
//}
insert = $.proxy(function(dataUrl, width, height, isResized) {
//console.log('dataUrl', dataUrl)
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
this.log('image inserted')
result = dataUrl
if (this.options.imageWidth == 'absolute') {
img = "<img tabindex=\"0\" style=\"width: " + width + "px; max-width: 100%;\" src=\"" + result + "\">"
}
else {
img = "<img tabindex=\"0\" style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
}
this.paste(img)
}, this)
// resize if to big
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
}, this)
reader.readAsDataURL(imageFile)
return true
}
}
// insert html
if (htmlRaw) {
htmlString = App.Utils.clipboardHtmlInsertPreperation(htmlRaw, this.options)
if (htmlString) {
this.log('insert html from clipboard', htmlString)
this.paste(htmlString)
App.Utils.htmlImage2DataUrlAsyncInline(this.$element)
return true
}
}
// insert text
text = this.getTextFromClipboard(clipboardData)
if (!text) {
return false
}
htmlString = App.Utils.text2html(text)
// check length limit
if (!this.maxLengthOk(htmlString.length)) {
return
}
htmlString = App.Utils.removeEmptyLines(htmlString)
this.log('insert text from clipboard', htmlString)
this.paste(htmlString)
return true
}
Plugin.prototype.onDragover = function (e) {
e.stopPropagation()
e.preventDefault()
this.log('dragover')
}
Plugin.prototype.onDrop = function (e) {
e.stopPropagation();
e.preventDefault();
this.log('drop')
var dataTransfer
if (window.dataTransfer) { // ie
dataTransfer = window.dataTransfer
}
else if (e.originalEvent.dataTransfer) { // other browsers
dataTransfer = e.originalEvent.dataTransfer
}
else {
throw "No clipboardData support"
}
// x and y coordinates of dropped item
x = e.clientX
y = e.clientY
var file = dataTransfer.files[0]
if(!file) return;
// look for images
if (file.type.match('image.*')) {
var reader = new FileReader()
reader.onload = (function(e) {
var result = e.target.result
var img = document.createElement('img')
img.src = result
maxWidth = this.$element.width() || 500
scaleFactor = 2
//scaleFactor = 1
//if (window.isRetina && window.isRetina()) {
// scaleFactor = 2
//}
//Insert the image at the carat
insert = function(dataUrl, width, height, isResized) {
//console.log('dataUrl', dataUrl)
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
this.log('image inserted')
result = dataUrl
if (this.options.imageWidth == 'absolute') {
img = "<img tabindex=\"0\" style=\"width: " + width + "px; max-width: 100%;\" src=\"" + result + "\">"
}
else {
img = "<img tabindex=\"0\" style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
}
if (document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y)
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset)
range.collapse()
range.insertNode(img)
}
else if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y)
range.insertNode(img)
}
else {
console.log('could not find carat')
}
}
// resize if to big
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
})
reader.readAsDataURL(file)
}
}
// check if key is allowed, even if length limit is reached
Plugin.prototype.allowKey = function(e) {
if ( this.options.allowKey[ e.keyCode ] ) {
return true
}
if ( ( e.ctrlKey || e.metaKey ) && this.options.extraAllowKey[ e.keyCode ] ) {
return true
}
return false
}
// max length check
Plugin.prototype.maxLengthOk = function(typeAhead) {
if ( !this.options.maxlength ) {
return true
}
var length = this.$element.text().length
if (typeAhead) {
length = length + typeAhead
}
this.log('maxLengthOk', length, this.options.maxlength)
if ( length > this.options.maxlength ) {
this.log('maxLengthOk, text too long')
// try to set error on framework form
var parent = this.$element.parent().parent()
if ( parent.hasClass('controls') ) {
parent.addClass('has-error')
setTimeout($.proxy(function(){
parent.removeClass('has-error')
}, this), 1000)
return false
}
// set validation on element
else {
this.$element.addClass('invalid')
setTimeout($.proxy(function(){
this.$element.removeClass('invalid')
}, this), 1000)
return false
}
}
return true
}
// get value
Plugin.prototype.value = function() {
//this.updatePlaceholder( 'remove' )
// get text
if ( this.options.mode === 'textonly' ) {
// strip html signes if multi line exists
if ( this.options.multiline ) {
// for validation, do not retrun empty content by empty tags
text_plain = this.$element.text().trim()
if ( !text_plain || text_plain == '' ) {
return text_plain
}
return this.$element.html()
}
return this.$element.text().trim()
}
// for validation, do not retrun empty content by empty tags
text_plain = this.$element.text().trim()
if ( (!text_plain || text_plain == '') && !this.$element.find('img').get(0) ) {
return text_plain
}
return this.$element.html().trim()
}
// log method
Plugin.prototype.log = function() {
if (App && App.Log) {
App.Log.debug('contenteditable', arguments)
}
if (this.options.debug) {
console.log(this._name, arguments)
}
}
// paste some content
Plugin.prototype.paste = function(string) {
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
// IE <= 10
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange()
if (range.pasteHTML) {
range.pasteHTML(string)
}
}
// IE == 11
else if (isIE11 && document.getSelection) {
var range = document.getSelection().getRangeAt(0)
var nnode = document.createElement('div')
range.surroundContents(nnode)
nnode.innerHTML = string
}
else {
document.execCommand('insertHTML', false, string)
}
}
$.fn[pluginName] = function (options) {
return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName,
new Plugin( this, options ));
}
});
}
// get correct val if textbox
$.fn.ceg = function() {
var plugin = $.data(this[0], 'plugin_' + pluginName)
if (!plugin) {
return
}
return plugin.value()
}
}(jQuery));