From 76f261ecb3374775dee08ea8b1df5e3c6651d460 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Thu, 15 Feb 2018 17:08:04 +0100 Subject: [PATCH 1/3] contenteditable & textmodule: refactor bindEvents --- .../app/lib/base/jquery.contenteditable.js | 690 +++++++++--------- .../app/lib/base/jquery.textmodule.js | 275 +++---- 2 files changed, 486 insertions(+), 479 deletions(-) diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js index 85618ba36..cc4139e3e 100644 --- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js +++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js @@ -66,402 +66,406 @@ this.browserMagicKey = App.Browser.magicKey() this.browserHotkeys = App.Browser.hotkeys() - this.init(); + this.init() } Plugin.prototype.init = function () { - var _this = this + this.bindEvents() + } - this.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') + 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 } - else { - document.execCommand('formatBlock', false, tag) + + // break
after enter on empty line + sel = window.getSelection() + if (sel) { + node = $(sel.anchorNode) + if (node && node.parent() && node.parent().is('blockquote')) { + 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('

') + return } } - // handle enter - this.$element.on('keydown', function (e) { - _this.log('keydown', e.keyCode) - if (_this.preventInput) { - this.log('preventInput', _this.preventInput) - 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 } - - // strap the return key being pressed - if (e.keyCode === 13) { - - // disbale multi line - if (!_this.options.multiline) { - e.preventDefault() - return - } - - // break
after enter on empty line - sel = window.getSelection() - if (sel) { - node = $(sel.anchorNode) - if (node && node.parent() && node.parent().is('blockquote')) { - 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('

') - return - } + } + else { + if (!e.altKey && e.ctrlKey && !e.metaKey) { + richtTextControl = true } - - // 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 == 86 - || 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 == 86) { - 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) + } + if (richtTextControl && this.options.richTextFormatKey[ e.keyCode ]) { + e.preventDefault() + if (e.keyCode == 66) { + document.execCommand('bold') return true } - - // limit check - if ( !_this.allowKey(e) ) { - if ( !_this.maxLengthOk(1) ) { - e.preventDefault() - return - } + 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 + } + } - // just paste text - this.$element.on('paste', function (e) { + 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 == 86 + || e.keyCode == 88 + || e.keyCode == 90 + || e.keyCode == 89)) { e.preventDefault() - _this.log('paste') - // insert and in case, resize images - 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" - } - - if (clipboardData && clipboardData.items && clipboardData.items[0]) { - var imageInserted = false - var item = clipboardData.items[0] - if (item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')) { - _this.log('paste image', item) - console.log(item) - - var imageFile = item.getAsFile() - 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 = 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 = "" - } - else { - img = "" - } - _this.paste(img) - } - - // resize if to big - App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert) - } - reader.readAsDataURL(imageFile) - imageInserted = true - } - } - if (imageInserted) { + // disable rich text b/u/i + if ( this.options.mode === 'textonly' ) { return } - // check existing + paste text for limit - var text, docType - try { - text = clipboardData.getData('text/html') - docType = 'html' - if (!text || text.length === 0) { - docType = 'text' - text = clipboardData.getData('text/plain') - } - if (!text || text.length === 0) { - docType = 'text2' - text = clipboardData.getData('text') - } + if (e.keyCode == 49) { + this.toggleBlock('h1') } - catch (e) { - console.log('Sorry, can\'t insert markup because browser is not supporting it.') - docType = 'text3' - text = clipboardData.getData('text') + if (e.keyCode == 50) { + this.toggleBlock('h2') } - _this.log('paste', docType, text) - - if (docType == 'html') { - if (_this.options.mode === 'textonly') { - if (!_this.options.multiline) { - text = App.Utils.htmlRemoveTags(text) - _this.log('htmlRemoveTags', text) - } - else { - _this.log('htmlRemoveRichtext', text) - text = App.Utils.htmlRemoveRichtext(text) - } - } - else { - _this.log('htmlCleanup', text) - text = App.Utils.htmlCleanup(text) - } - text = text.html() - _this.log('text.html()', text) - - // as fallback, take text - if (!text) { - text = App.Utils.text2html(text.text()) - _this.log('text2html', text) - } + if (e.keyCode == 51) { + this.toggleBlock('h3') } - else { - text = App.Utils.text2html(text) - _this.log('text2html', text) + if (e.keyCode == 66) { + document.execCommand('bold') } - - if (!_this.maxLengthOk(text.length)) { - return + if (e.keyCode == 70) { + document.execCommand('removeFormat') } - - // cleanup - text = App.Utils.removeEmptyLines(text) - _this.log('insert', text) - - _this.paste(text) + 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 == 86) { + 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 - }) + } - this.$element.on('dragover', function (e) { - e.stopPropagation() - e.preventDefault() - _this.log('dragover') - }) - - this.$element.on('drop', 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" + // limit check + if ( !this.allowKey(e) ) { + if ( !this.maxLengthOk(1) ) { + e.preventDefault() + return } + } + } - // x and y coordinates of dropped item - x = e.clientX - y = e.clientY - var file = dataTransfer.files[0] + Plugin.prototype.onPaste = function (e) { + e.preventDefault() + this.log('paste') - // look for images - if (file.type.match('image.*')) { + // insert and in case, resize images + 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" + } + + if (clipboardData && clipboardData.items && clipboardData.items[0]) { + var imageInserted = false + var item = clipboardData.items[0] + if (item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')) { + this.log('paste image', item) + console.log(item) + + var imageFile = item.getAsFile() var reader = new FileReader() - reader.onload = (function(e) { + + reader.onload = function (e) { var result = e.target.result var img = document.createElement('img') img.src = result - maxWidth = _this.$element.width() || 500 + 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') + this.log('image inserted') result = dataUrl - if (_this.options.imageWidth == 'absolute') { - img = $("") + if (this.options.imageWidth == 'absolute') { + img = "" } else { - img = $("") - } - img = img.get(0) - - 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') + img = "" } + this.paste(img) } // resize if to big App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert) - }) - reader.readAsDataURL(file) + } + reader.readAsDataURL(imageFile) + imageInserted = true } - }) + } + if (imageInserted) { + return + } + // check existing + paste text for limit + var text, docType + try { + text = clipboardData.getData('text/html') + docType = 'html' + if (!text || text.length === 0) { + docType = 'text' + text = clipboardData.getData('text/plain') + } + if (!text || text.length === 0) { + docType = 'text2' + text = clipboardData.getData('text') + } + } + catch (e) { + console.log('Sorry, can\'t insert markup because browser is not supporting it.') + docType = 'text3' + text = clipboardData.getData('text') + } + this.log('paste', docType, text) + + if (docType == 'html') { + if (this.options.mode === 'textonly') { + if (!this.options.multiline) { + text = App.Utils.htmlRemoveTags(text) + this.log('htmlRemoveTags', text) + } + else { + this.log('htmlRemoveRichtext', text) + text = App.Utils.htmlRemoveRichtext(text) + } + } + else { + this.log('htmlCleanup', text) + text = App.Utils.htmlCleanup(text) + } + text = text.html() + this.log('text.html()', text) + + // as fallback, take text + if (!text) { + text = App.Utils.text2html(text.text()) + this.log('text2html', text) + } + } + else { + text = App.Utils.text2html(text) + this.log('text2html', text) + } + + if (!this.maxLengthOk(text.length)) { + return + } + + // cleanup + text = App.Utils.removeEmptyLines(text) + this.log('insert', text) + + this.paste(text) + 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] + + // 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 = $("") + } + else { + img = $("") + } + img = img.get(0) + + 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 diff --git a/app/assets/javascripts/app/lib/base/jquery.textmodule.js b/app/assets/javascripts/app/lib/base/jquery.textmodule.js index 40b95fffe..377461abf 100644 --- a/app/assets/javascripts/app/lib/base/jquery.textmodule.js +++ b/app/assets/javascripts/app/lib/base/jquery.textmodule.js @@ -34,160 +34,163 @@ this.ce = $.data(element, 'plugin_ce') } - this.init(); + this.init() } Plugin.prototype.init = function () { this.renderBase() - var _this = this + this.bindEvents() + } - this.$element.on('keydown', function (e) { + Plugin.prototype.bindEvents = function () { + this.$element.on('keydown', this.onKeydown.bind(this)) + this.$element.on('keypress', this.onKeypress.bind(this)) + this.$element.on('focus', this.onFocus.bind(this)) + } - // navigate through item - if (_this.isActive()) { + Plugin.prototype.onFocus = function (e) { + this.close() + } - // esc - if (e.keyCode === 27) { - e.preventDefault() - e.stopPropagation() - _this.close() - return - } - - // enter - if (e.keyCode === 13) { - e.preventDefault() - e.stopPropagation() - var id = _this.$widget.find('.dropdown-menu li.is-active').data('id') - - // as fallback use hovered element - if (!id) { - id = _this.$widget.find('.dropdown-menu li:hover').data('id') - } - - // as fallback first element - if (!id) { - id = _this.$widget.find('.dropdown-menu li:first-child').data('id') - } - _this.take(id) - return - } - - // arrow keys left/right - if (e.keyCode === 37 || e.keyCode === 39) { - e.preventDefault() - e.stopPropagation() - return - } - - // up or down - if (e.keyCode === 38 || e.keyCode === 40) { - e.preventDefault() - e.stopPropagation() - var active = _this.$widget.find('.dropdown-menu li.is-active') - active.removeClass('is-active') - - if (e.keyCode == 38 && active.prev().size()) { - active = active.prev() - } - else if (e.keyCode == 40 && active.next().size()) { - active = active.next() - } - - active.addClass('is-active') - - var menu = _this.$widget.find('.dropdown-menu') - - if (!active.get(0)) { - return - } - if (active.position().top < 0) { - // scroll up - menu.scrollTop( menu.scrollTop() + active.position().top ) - } - else if ( active.position().top + active.height() > menu.height() ) { - // scroll down - var invisibleHeight = active.position().top + active.height() - menu.height() - menu.scrollTop( menu.scrollTop() + invisibleHeight ) - } - } - } + Plugin.prototype.onKeydown = function (e) { + console.log("onKeydown", this.isActive()) + // navigate through item + if (this.isActive()) { // esc if (e.keyCode === 27) { - _this.close() + e.preventDefault() + e.stopPropagation() + this.close() + return } - }) - - // reduce buffer, in case close it - this.$element.on('keydown', function (e) { - - // backspace - if (e.keyCode === 8 && _this.buffer) { - - // backspace + buffer === :: -> close textmodule - if (_this.buffer === '::') { - _this.close(true) - e.preventDefault() - return - } - - // reduce buffer and show new result - var length = _this.buffer.length - _this.buffer = _this.buffer.substr(0, length-1) - _this.log('BS backspace', _this.buffer) - _this.result(_this.buffer.substr(2, length-1)) - } - }) - - // build buffer - this.$element.on('keypress', function (e) { - _this.log('BUFF', _this.buffer, e.keyCode, String.fromCharCode(e.which)) - - // shift - if (e.keyCode === 16) return // enter - if (e.keyCode === 13) return + if (e.keyCode === 13) { + e.preventDefault() + e.stopPropagation() + var id = this.$widget.find('.dropdown-menu li.is-active').data('id') - // arrow keys - if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) return - - // observer other second key - if (_this.buffer === ':' && String.fromCharCode(e.which) !== ':') { - _this.buffer = '' - } - - // oberserve second : - if (_this.buffer === ':' && String.fromCharCode(e.which) === ':') { - _this.buffer = _this.buffer + ':' - } - - // oberserve first : - if (!_this.buffer && String.fromCharCode(e.which) === ':') { - _this.buffer = _this.buffer + ':' - } - - if (_this.buffer && _this.buffer.substr(0,2) === '::') { - - var sign = String.fromCharCode(e.which) - if ( sign && sign !== ':' && e.which != 8 ) { // 8 == backspace - _this.buffer = _this.buffer + sign - //_this.log('BUFF ADD', sign, this.buffer, sign.length, e.which) - } - _this.log('BUFF HINT', _this.buffer, _this.buffer.length, e.which, String.fromCharCode(e.which)) - - if (!_this.isActive()) { - _this.open() + // as fallback use hovered element + if (!id) { + id = this.$widget.find('.dropdown-menu li:hover').data('id') } - _this.result(_this.buffer.substr(2, _this.buffer.length)) + // as fallback first element + if (!id) { + id = this.$widget.find('.dropdown-menu li:first-child').data('id') + } + this.take(id) + return } - }).on('focus', function (e) { - _this.close() - }) - }; + // arrow keys left/right + if (e.keyCode === 37 || e.keyCode === 39) { + e.preventDefault() + e.stopPropagation() + return + } + + // up or down + if (e.keyCode === 38 || e.keyCode === 40) { + e.preventDefault() + e.stopPropagation() + var active = this.$widget.find('.dropdown-menu li.is-active') + active.removeClass('is-active') + + if (e.keyCode == 38 && active.prev().size()) { + active = active.prev() + } + else if (e.keyCode == 40 && active.next().size()) { + active = active.next() + } + + active.addClass('is-active') + + var menu = this.$widget.find('.dropdown-menu') + + if (!active.get(0)) { + return + } + if (active.position().top < 0) { + // scroll up + menu.scrollTop( menu.scrollTop() + active.position().top ) + } + else if ( active.position().top + active.height() > menu.height() ) { + // scroll down + var invisibleHeight = active.position().top + active.height() - menu.height() + menu.scrollTop( menu.scrollTop() + invisibleHeight ) + } + } + } + + // esc + if (e.keyCode === 27) { + this.close() + } + + // reduce buffer, in case close it + // backspace + if (e.keyCode === 8 && this.buffer) { + + // backspace + buffer === :: -> close textmodule + if (this.buffer === '::') { + this.close(true) + e.preventDefault() + return + } + + // reduce buffer and show new result + var length = this.buffer.length + this.buffer = this.buffer.substr(0, length-1) + this.log('BS backspace', this.buffer) + this.result(this.buffer.substr(2, length-1)) + } + } + + Plugin.prototype.onKeypress = function (e) { + this.log('BUFF', this.buffer, e.keyCode, String.fromCharCode(e.which)) + + // shift + if (e.keyCode === 16) return + + // enter + if (e.keyCode === 13) return + + // arrow keys + if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) return + + // observer other second key + if (this.buffer === ':' && String.fromCharCode(e.which) !== ':') { + this.buffer = '' + } + + // oberserve second : + if (this.buffer === ':' && String.fromCharCode(e.which) === ':') { + this.buffer = this.buffer + ':' + } + + // oberserve first : + if (!this.buffer && String.fromCharCode(e.which) === ':') { + this.buffer = this.buffer + ':' + } + + if (this.buffer && this.buffer.substr(0,2) === '::') { + + var sign = String.fromCharCode(e.which) + if ( sign && sign !== ':' && e.which != 8 ) { // 8 == backspace + this.buffer = this.buffer + sign + //this.log('BUFF ADD', sign, this.buffer, sign.length, e.which) + } + this.log('BUFF HINT', this.buffer, this.buffer.length, e.which, String.fromCharCode(e.which)) + + if (!this.isActive()) { + this.open() + } + + this.result(this.buffer.substr(2, this.buffer.length)) + } + } // create base template Plugin.prototype.renderBase = function() { From 15e9fe71866e6ee0b5f84385d3dc42a53aeaef28 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Thu, 15 Feb 2018 19:13:21 +0100 Subject: [PATCH 2/3] enableObjectResizing shim 1. step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit open bugs: the blur event doesn’t fire. so when clicking into the text after the controls appeared the controls don’t disappear as they should. also when clicking onto another image when the controls of one image are visible first the controls of the active image dissapear and the controls of the current image don’t directly disappear --- .../app/lib/base/jquery.contenteditable.js | 18 +-- .../base/jquery.enableObjectResizingShim.js | 105 ++++++++++++++++++ app/assets/stylesheets/application.css | 1 + .../jquery.enableObjectResizingShim.css | 77 +++++++++++++ 4 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js create mode 100644 app/assets/stylesheets/jquery.enableObjectResizingShim.css diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js index cc4139e3e..05b6d5567 100644 --- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js +++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js @@ -72,6 +72,7 @@ Plugin.prototype.init = function () { this.bindEvents() + this.$element.enableObjectResizingShim() } Plugin.prototype.bindEvents = function () { @@ -291,7 +292,7 @@ var imageFile = item.getAsFile() var reader = new FileReader() - reader.onload = function (e) { + reader.onload = $.proxy(function (e) { var result = e.target.result var img = document.createElement('img') img.src = result @@ -302,23 +303,24 @@ // scaleFactor = 2 //} - insert = function(dataUrl, width, height, isResized) { + 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 = "" } else { - img = "" + img = "" } this.paste(img) - } + }, this) // resize if to big App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert) - } + }, this) + reader.readAsDataURL(imageFile) imageInserted = true } @@ -438,10 +440,10 @@ this.log('image inserted') result = dataUrl if (this.options.imageWidth == 'absolute') { - img = $("") + img = $("") } else { - img = $("") + img = $("") } img = img.get(0) diff --git a/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js b/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js new file mode 100644 index 000000000..be889d775 --- /dev/null +++ b/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js @@ -0,0 +1,105 @@ +(function ($, window, undefined) { + +/* +# 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' +# +*/ + + var pluginName = 'enableObjectResizingShim', + defaults = { + debug: false + } + + function Plugin(element, options) { + this.element = element + this.$element = $(element) + + this.options = $.extend({}, defaults, options) + + this._defaults = defaults + this._name = pluginName + + this.isActive = false + + // only run if needed + if (!document.queryCommandSupported('enableObjectResizing')) { + this.init() + } + } + + Plugin.prototype.init = function () { + this.bindEvents() + } + + Plugin.prototype.bindEvents = function () { + this.$element.on('focus', 'img', this.addResizeHandles.bind(this)) + this.$element.on('blur', 'img', this.removeResizeHandles.bind(this)) + this.$element.on('mousedown', '.enableObjectResizingShim-handle', this.startResize.bind(this)) + } + + Plugin.prototype.addResizeHandles = function (event) { + if(this.isActive) return + var $img = $(event.currentTarget) + var $holder = $('
') + $img.wrap($holder) + + for (var i=0; i<4; i++) { + $img.before('
') + } + + this.isActive = true + } + + Plugin.prototype.removeResizeHandles = function (event) { + console.log("removeResizeHandles") + if(this.isResizing) return + var $img = this.$element.find('.enableObjectResizingShim img') + $img.siblings().remove() + $img.unwrap() + this.isActive = false + } + + Plugin.prototype.startResize = function (event) { + $(document).on('mousemove.enableObjectResizing', this.resize.bind(this)) + $(document).on('mouseup.enableObjectResizing', this.resizeEnd.bind(this)) + var $handle = $(event.currentTarget) + this.resizeCorner = $handle.index() + this.$img = this.$element.find('.enableObjectResizingShim img') + this.startX = event.pageX + this.startWidth = this.$img.width() + this.$clone = this.$img.clone().css({width: '', height: ''}).addClass('enableObjectResizingShim-clone enableObjectResizingShim-clone--'+ this.resizeCorner) + this.$img.after(this.$clone) + this.isResizing = true + } + + Plugin.prototype.resize = function (event) { + event.preventDefault() + var dx = event.pageX - this.startX + this.$clone.css('width', this.startWidth + dx) + } + + Plugin.prototype.resizeEnd = function (event) { + $(document).off('mousemove.enableObjectResizing') + $(document).off('mouseup.enableObjectResizing') + + this.$img.css({ + width: this.$clone.width(), + height: this.$clone.height() + }) + this.$clone.remove() + this.isResizing = false + } + + $.fn[pluginName] = function (options) { + return this.each(function () { + if (!$.data(this, 'plugin_' + pluginName)) { + $.data(this, 'plugin_' + pluginName, new Plugin(this, options)) + } + }); + } + +}(jQuery, window)); \ No newline at end of file diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 020860823..809f2b57b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -10,6 +10,7 @@ *= require ./font.css *= require ./svg-dimensions.css *= require ./highlighter-github.css + *= require ./jquery.enableObjectResizingShim.css *= require ./zammad.scss * *= require_tree ./custom/ diff --git a/app/assets/stylesheets/jquery.enableObjectResizingShim.css b/app/assets/stylesheets/jquery.enableObjectResizingShim.css new file mode 100644 index 000000000..bce47d8b4 --- /dev/null +++ b/app/assets/stylesheets/jquery.enableObjectResizingShim.css @@ -0,0 +1,77 @@ +.enableObjectResizingShim { + box-shadow: 0 0 0 1px black; + position: relative; + display: inline-block !important; + vertical-align: top; +} + +.enableObjectResizingShim-handle { + position: absolute; + width: 20px; + height: 20px; + margin: -10px; + left: 0; + top: 0; + cursor: nwse-resize; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.enableObjectResizingShim-handle:after { + content: ""; + position: absolute; + left: 6px; + top: 6px; + width: 8px; + height: 8px; + background: white; + border: 1px solid black; +} + +.enableObjectResizingShim-handle:hover:after { + background: black; +} + +.enableObjectResizingShim-handle:nth-child(2) { + left: 100%; + cursor: nesw-resize; +} + +.enableObjectResizingShim-handle:nth-child(3) { + left: 100%; + top: 100%; +} + +.enableObjectResizingShim-handle:nth-child(4) { + top: 100%; + cursor: nesw-resize; +} + +.enableObjectResizingShim-clone { + position: absolute; + width: 100%; + height: auto; + opacity: 0.5; + border: 1px dashed black; +} + +.enableObjectResizingShim-clone--0 { + right: 0; + bottom: 0; +} + +.enableObjectResizingShim-clone--1 { + left: 0; + bottom: 0; +} + +.enableObjectResizingShim-clone--2 { + left: 0; + top: 0; +} + +.enableObjectResizingShim-clone--3 { + top: 0; + right: 0; +} \ No newline at end of file From fa449de9ed35908d85e8772e5c56292839fbec7e Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Fri, 16 Feb 2018 14:08:32 +0100 Subject: [PATCH 3/3] enableObjectResizingShim: fix deselect & allow delete - fix deselect when clicking somewhere else - allow to delete the image via backspace --- .../base/jquery.enableObjectResizingShim.js | 118 +++++++++++++----- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js b/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js index be889d775..7f6ad9fd9 100644 --- a/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js +++ b/app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js @@ -23,77 +23,127 @@ this._defaults = defaults this._name = pluginName - this.isActive = false - // only run if needed if (!document.queryCommandSupported('enableObjectResizing')) { - this.init() + this.bindEvents() } } - Plugin.prototype.init = function () { - this.bindEvents() - } - Plugin.prototype.bindEvents = function () { - this.$element.on('focus', 'img', this.addResizeHandles.bind(this)) - this.$element.on('blur', 'img', this.removeResizeHandles.bind(this)) - this.$element.on('mousedown', '.enableObjectResizingShim-handle', this.startResize.bind(this)) + this.$element.on('click', 'img', this.createEditor.bind(this)) + this.$element.on('click', this.destroyEditors.bind(this)) } - Plugin.prototype.addResizeHandles = function (event) { - if(this.isActive) return + Plugin.prototype.createEditor = function (event) { + event.stopPropagation() + this.destroyEditors() var $img = $(event.currentTarget) - var $holder = $('
') - $img.wrap($holder) + + if (!$img.hasClass('objectResizingEditorActive')) { + new Editor($img) + } + } + + Plugin.prototype.destroyEditors = function () { + this.$element.find('img.objectResizingEditorActive').each(function(i, img){ + editor = $(img).data('objectResizingEditor') + if(editor){ + editor.destroy() + } + }) + } + + + + /* + + Resize Editor + + */ + + function Editor($element) { + this.$element = $element + this.isResizing = false + + this.$element.data('objectResizingEditor', this) + this.$element.addClass('objectResizingEditorActive') + this.$element.wrap('
') for (var i=0; i<4; i++) { - $img.before('
') + this.$element.before('
') } - this.isActive = true + $(document).one('click.objectResizingEditor', this.destroy.bind(this)) + $(document).one('keydown.objectResizingEditor', this.onKeydown.bind(this)) + this.$element.on('click.objectResizingEditor', this.stopPropagation.bind(this)) + this.$element.parent().find('.enableObjectResizingShim-handle').on('mousedown', this.startResize.bind(this)) } - Plugin.prototype.removeResizeHandles = function (event) { - console.log("removeResizeHandles") + Editor.prototype.onKeydown = function (event) { + this.destroy() + + switch (event.keyCode) { + case 8: // backspace + this.$element.remove() + break + default: + event.stopPropagation() + break + } + } + + Editor.prototype.stopPropagation = function (event) { + event.stopPropagation() + } + + Editor.prototype.destroy = function (event) { if(this.isResizing) return - var $img = this.$element.find('.enableObjectResizingShim img') - $img.siblings().remove() - $img.unwrap() - this.isActive = false + this.$element.off('click.objectResizingEditor') + $(document).off('click.objectResizingEditor') + $(document).off('keydown.objectResizingEditor') + this.$element.removeClass('objectResizingEditorActive') + this.$element.siblings().remove() + this.$element.unwrap() } - Plugin.prototype.startResize = function (event) { - $(document).on('mousemove.enableObjectResizing', this.resize.bind(this)) - $(document).on('mouseup.enableObjectResizing', this.resizeEnd.bind(this)) + Editor.prototype.startResize = function (event) { var $handle = $(event.currentTarget) this.resizeCorner = $handle.index() - this.$img = this.$element.find('.enableObjectResizingShim img') + this.resizeDir = this.resizeCorner == 0 || this.resizeCorner == 3 ? -1 : 1 this.startX = event.pageX - this.startWidth = this.$img.width() - this.$clone = this.$img.clone().css({width: '', height: ''}).addClass('enableObjectResizingShim-clone enableObjectResizingShim-clone--'+ this.resizeCorner) - this.$img.after(this.$clone) + this.startWidth = this.$element.width() + this.$clone = this.$element.clone().css({width: '', height: ''}).addClass('enableObjectResizingShim-clone enableObjectResizingShim-clone--'+ this.resizeCorner) + this.$element.after(this.$clone) this.isResizing = true + $(document).on('mousemove.enableObjectResizing', this.resize.bind(this)) + $(document).on('mouseup.enableObjectResizing', this.resizeEnd.bind(this)) } - Plugin.prototype.resize = function (event) { + Editor.prototype.resize = function (event) { event.preventDefault() var dx = event.pageX - this.startX - this.$clone.css('width', this.startWidth + dx) + this.$clone.css('width', this.startWidth + (this.resizeDir * dx)) } - Plugin.prototype.resizeEnd = function (event) { + Editor.prototype.resizeEnd = function (event) { $(document).off('mousemove.enableObjectResizing') $(document).off('mouseup.enableObjectResizing') - this.$img.css({ + this.$element.css({ width: this.$clone.width(), height: this.$clone.height() }) this.$clone.remove() - this.isResizing = false + + // reset isResizing with a delay to prevent a mouseup in the editor to trigger a editor-destroy + setTimeout(function(){ + this.isResizing = false + }.bind(this)) } + + + $.fn[pluginName] = function (options) { return this.each(function () { if (!$.data(this, 'plugin_' + pluginName)) {