Browse Source

Merge branch 'private-richtext-object-resizing-shim' into develop

Martin Edenhofer 7 years ago
parent
commit
fa53eeb33a

+ 337 - 331
app/assets/javascripts/app/lib/base/jquery.contenteditable.js

@@ -66,402 +66,408 @@
     this.browserMagicKey = App.Browser.magicKey()
     this.browserHotkeys = App.Browser.hotkeys()
 
-    this.init();
+    this.init()
   }
 
 
   Plugin.prototype.init = function () {
-    var _this = this
+    this.bindEvents()
+    this.$element.enableObjectResizingShim()
+  }
 
-    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')
-      }
-      else {
-        document.execCommand('formatBlock', false, tag)
-      }
+  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
     }
 
-    // handle enter
-    this.$element.on('keydown', function (e) {
-      _this.log('keydown', e.keyCode)
-      if (_this.preventInput) {
-        this.log('preventInput', _this.preventInput)
+    // strap the return key being pressed
+    if (e.keyCode === 13) {
+
+      // disbale multi line
+      if (!this.options.multiline) {
+        e.preventDefault()
         return
       }
 
-      // strap the return key being pressed
-      if (e.keyCode === 13) {
-
-        // disbale multi line
-        if (!_this.options.multiline) {
+      // break <blockquote> 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
         }
+      }
 
-        // break <blockquote> 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('<br><br>')
-          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
-        }
+    // 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
-        }
+    }
+    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
-        }
+    }
+    if (richtTextControl && this.options.richTextFormatKey[ e.keyCode ]) {
+      e.preventDefault()
+      if (e.keyCode == 66) {
+        document.execCommand('bold')
+        return true
       }
-
-      var hotkeys = false
-      if (_this.browserHotkeys == 'ctrl+shift') {
-        if (!e.altKey && e.ctrlKey && !e.metaKey && e.shiftKey) {
-          hotkeys = true
-        }
+      if (e.keyCode == 73) {
+        document.execCommand('italic')
+        return true
       }
-      else {
-        if (e.altKey && e.ctrlKey && !e.metaKey) {
-          hotkeys = true
-        }
+      if (e.keyCode == 85) {
+        document.execCommand('underline')
+        return 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 (e.keyCode == 83) {
+        document.execCommand('strikeThrough')
         return true
       }
+    }
 
-      // limit check
-      if ( !_this.allowKey(e) ) {
-        if ( !_this.maxLengthOk(1) ) {
-          e.preventDefault()
-          return
-        }
+    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
+      }
+    }
 
-    // just paste text
-    this.$element.on('paste', function (e) {
+    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
+      // disable rich text b/u/i
+      if ( this.options.mode === 'textonly' ) {
+        return
       }
-      else if (window.clipboardData) { // ie
-        clipboardData = window.clipboardData
+
+      if (e.keyCode == 49) {
+        this.toggleBlock('h1')
       }
-      else if (e.originalEvent.clipboardData) { // other browsers
-        clipboardData = e.originalEvent.clipboardData
+      if (e.keyCode == 50) {
+        this.toggleBlock('h2')
       }
-      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 = "<img style=\"width: " + width + "px; height: " + height + "px\" src=\"" + result + "\">"
-              }
-              else {
-                img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
-              }
-              _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 (e.keyCode == 51) {
+        this.toggleBlock('h3')
       }
-      if (imageInserted) {
-        return
+      if (e.keyCode == 66) {
+        document.execCommand('bold')
       }
-
-      // 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 == 70) {
+        document.execCommand('removeFormat')
       }
-      catch (e) {
-        console.log('Sorry, can\'t insert markup because browser is not supporting it.')
-        docType = 'text3'
-        text = clipboardData.getData('text')
+      if (e.keyCode == 73) {
+        document.execCommand('italic')
       }
-      _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 == 75) {
+        document.execCommand('insertOrderedList')
       }
-      else {
-        text = App.Utils.text2html(text)
-        _this.log('text2html', text)
+      if (e.keyCode == 76) {
+        document.execCommand('insertUnorderedList')
       }
-
-      if (!_this.maxLengthOk(text.length)) {
-        return
+      if (e.keyCode == 85) {
+        document.execCommand('underline')
       }
-
-      // cleanup
-      text = App.Utils.removeEmptyLines(text)
-      _this.log('insert', text)
-
-      _this.paste(text)
+      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')
-    })
+    // limit check
+    if ( !this.allowKey(e) ) {
+      if ( !this.maxLengthOk(1) ) {
+        e.preventDefault()
+        return
+      }
+    }
+  }
 
-    this.$element.on('drop', function (e) {
-      e.stopPropagation();
-      e.preventDefault();
-      _this.log('drop')
+  Plugin.prototype.onPaste = function (e) {
+    e.preventDefault()
+    this.log('paste')
 
-      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"
-      }
+    // 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"
+    }
 
-      // x and y coordinates of dropped item
-      x = e.clientX
-      y = e.clientY
-      var file = dataTransfer.files[0]
+    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)
 
-      // look for images
-      if (file.type.match('image.*')) {
+        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
-          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) {
-
+          insert = $.proxy(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 = $("<img style=\"width: " + width + "px; height: " + height + "px\" src=\"" + result + "\">")
-            }
-            else {
-              img = $("<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">")
-            }
-            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)
+            if (this.options.imageWidth == 'absolute') {
+              img = "<img tabindex=\"0\" style=\"width: " + width + "px; height: " + height + "px\" src=\"" + result + "\">"
             }
             else {
-              console.log('could not find carat')
+              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)
-        })
-        reader.readAsDataURL(file)
+        }, this)
+
+        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 = $("<img tabindex=\"0\" style=\"width: " + width + "px; height: " + height + "px\" src=\"" + result + "\">")
+          }
+          else {
+            img = $("<img tabindex=\"0\" style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">")
+          }
+          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

+ 155 - 0
app/assets/javascripts/app/lib/base/jquery.enableObjectResizingShim.js

@@ -0,0 +1,155 @@
+(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
+
+    // only run if needed
+    if (!document.queryCommandSupported('enableObjectResizing')) {
+      this.bindEvents()
+    }
+  }
+
+  Plugin.prototype.bindEvents = function () {
+    this.$element.on('click', 'img', this.createEditor.bind(this))
+    this.$element.on('click', this.destroyEditors.bind(this))
+  }
+
+  Plugin.prototype.createEditor = function (event) {
+    event.stopPropagation()
+    this.destroyEditors()
+    var $img = $(event.currentTarget)
+
+    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('<div class="enableObjectResizingShim" contenteditable="false"></div>')
+
+    for (var i=0; i<4; i++) {
+      this.$element.before('<div class="enableObjectResizingShim-handle"></div>')
+    }
+
+    $(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))
+  }
+
+  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
+    this.$element.off('click.objectResizingEditor')
+    $(document).off('click.objectResizingEditor')
+    $(document).off('keydown.objectResizingEditor')
+    this.$element.removeClass('objectResizingEditorActive')
+    this.$element.siblings().remove()
+    this.$element.unwrap()
+  }
+
+  Editor.prototype.startResize = function (event) {
+    var $handle = $(event.currentTarget)
+    this.resizeCorner = $handle.index()
+    this.resizeDir = this.resizeCorner == 0 || this.resizeCorner == 3 ? -1 : 1
+    this.startX = event.pageX
+    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))
+  }
+
+  Editor.prototype.resize = function (event) {
+    event.preventDefault()
+    var dx = event.pageX - this.startX
+    this.$clone.css('width', this.startWidth + (this.resizeDir * dx))
+  }
+
+  Editor.prototype.resizeEnd = function (event) {
+    $(document).off('mousemove.enableObjectResizing')
+    $(document).off('mouseup.enableObjectResizing')
+
+    this.$element.css({
+      width: this.$clone.width(),
+      height: this.$clone.height()
+    })
+    this.$clone.remove()
+
+    // 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)) {
+        $.data(this, 'plugin_' + pluginName, new Plugin(this, options))
+      }
+    });
+  }
+
+}(jQuery, window));

+ 122 - 119
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
-        }
+  Plugin.prototype.onKeydown = function (e) {
+    console.log("onKeydown", this.isActive())
+    // navigate through item
+    if (this.isActive()) {
 
-        // 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
-        }
+      // esc
+      if (e.keyCode === 27) {
+        e.preventDefault()
+        e.stopPropagation()
+        this.close()
+        return
+      }
 
-        // arrow keys left/right
-        if (e.keyCode === 37 || e.keyCode === 39) {
-          e.preventDefault()
-          e.stopPropagation()
-          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')
         }
 
-        // 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 )
-          }
+        // as fallback first element
+        if (!id) {
+          id = this.$widget.find('.dropdown-menu li:first-child').data('id')
         }
+        this.take(id)
+        return
       }
 
-      // esc
-      if (e.keyCode === 27) {
-        _this.close()
+      // arrow keys left/right
+      if (e.keyCode === 37 || e.keyCode === 39) {
+        e.preventDefault()
+        e.stopPropagation()
+        return
       }
-    })
 
-    // reduce buffer, in case close it
-    this.$element.on('keydown', function (e) {
+      // 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')
 
-      // backspace
-      if (e.keyCode === 8 && _this.buffer) {
+        var menu = this.$widget.find('.dropdown-menu')
 
-        // backspace + buffer === :: -> close textmodule
-        if (_this.buffer === '::') {
-          _this.close(true)
-          e.preventDefault()
+        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) {
 
-        // 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))
+      // backspace + buffer === :: -> close textmodule
+      if (this.buffer === '::') {
+        this.close(true)
+        e.preventDefault()
+        return
       }
-    })
 
-    // build buffer
-    this.$element.on('keypress', function (e) {
-      _this.log('BUFF', _this.buffer, e.keyCode, String.fromCharCode(e.which))
+      // 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))
+    }
+  }
 
-      // shift
-      if (e.keyCode === 16) return
+  Plugin.prototype.onKeypress = function (e) {
+    this.log('BUFF', this.buffer, e.keyCode, String.fromCharCode(e.which))
 
-      // enter
-      if (e.keyCode === 13) return
+    // shift
+    if (e.keyCode === 16) return
 
-      // arrow keys
-      if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) return
+    // enter
+    if (e.keyCode === 13) return
 
-      // observer other second key
-      if (_this.buffer === ':' && String.fromCharCode(e.which) !== ':') {
-        _this.buffer = ''
-      }
+    // arrow keys
+    if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) return
 
-      // oberserve second :
-      if (_this.buffer === ':' && String.fromCharCode(e.which) === ':') {
-        _this.buffer = _this.buffer + ':'
-      }
+    // observer other second key
+    if (this.buffer === ':' && String.fromCharCode(e.which) !== ':') {
+      this.buffer = ''
+    }
 
-      // oberserve first :
-      if (!_this.buffer && String.fromCharCode(e.which) === ':') {
-        _this.buffer = _this.buffer + ':'
-      }
+    // oberserve second :
+    if (this.buffer === ':' && String.fromCharCode(e.which) === ':') {
+      this.buffer = this.buffer + ':'
+    }
 
-      if (_this.buffer && _this.buffer.substr(0,2) === '::') {
+    // oberserve first :
+    if (!this.buffer && String.fromCharCode(e.which) === ':') {
+      this.buffer = this.buffer + ':'
+    }
 
-        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.buffer && this.buffer.substr(0,2) === '::') {
 
-        if (!_this.isActive()) {
-          _this.open()
-        }
+      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))
 
-        _this.result(_this.buffer.substr(2, _this.buffer.length))
+      if (!this.isActive()) {
+        this.open()
       }
 
-    }).on('focus', function (e) {
-      _this.close()
-    })
-  };
+      this.result(this.buffer.substr(2, this.buffer.length))
+    }
+  }
 
   // create base template
   Plugin.prototype.renderBase = function() {

+ 1 - 0
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/

+ 77 - 0
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;
+}