2026-06-23 10:00:25 +02:00
/ * *
* Quill - Modul : Drag - Resize für Bilder im Editor .
*
* Aktivierung pro Editor - Instanz via :
* modules : { imageResize : true }
*
* Verhalten :
* - Click auf < img > im Editor → 4 Ecken - Handles als position : fixed - Overlay
* - mousedown / touchstart auf einem Handle startet den Drag
* - Drag aktualisiert width - Attribut + Inline - Style mit ! important am < img >
* ( Inline - Style ist nötig , weil die globale ` img { max-width:100% } ` - Regel
* im Intranet das HTML - width - Attribut überstimmt — Spezifität 0 verliert
* gegen jede Author - CSS - Regel auf < img > )
* - Aspect Ratio über CSS ` height: auto ` gehalten
*
* Persistenz - Pipeline :
* 1 ) ResizableImage - Blot ( unten registriert ) whitelisted alt / width / height in
* Quills formats / image - Override , damit die Attribute den Quill - Save - Pfad
* ( ` quill.update ` ) überleben .
* 2 ) Pro Quill - Instanz : Clipboard - Matcher für IMG + Monkey - Patch um
* ` dangerouslyPasteHTML ` . Die reparieren den Lade - Pfad , weil Quill
* 2.0 . 0 - dev . 3 beim HTML → Delta - Parse die Attribute trotz Blot droppt .
* Der Monkey - Patch reappliziert width - Attribut UND Inline - ! important - Style
* am Editor - DOM ( gleiches Pattern wie Live - Drag ) .
*
* Bewusst klein gehalten . Keine Abhängigkeit zu third - party Resize - Modulen ,
* die für Quill 1.3 . x geschrieben wurden und in 2.0 . 0 - dev . 3 unstet sind .
* /
( function ( global ) {
2026-06-22 11:49:59 +02:00
'use strict' ;
2026-06-23 10:00:25 +02:00
if ( typeof Quill === 'undefined' ) {
console . error ( '[quill_resize] Quill ist nicht geladen.' ) ;
return ;
}
// ResizableImage-Blot: Quill 2.0.0-dev.3' nativer formats/image strippt
// beim Re-Parse alle Attribute außer src. Dieser Blot whitelisted alt,
// width, height, damit die per Drag gesetzte Größe den Roundtrip überlebt.
//
// Quill 2.x' Blots sind ES6-Klassen — Vererbung MUSS via `class ... extends`
// erfolgen, nicht via Prototype-Chain (Image.apply würde TypeError werfen,
// weil ES6-Classes nicht ohne `new` aufgerufen werden können).
//
// try/catch: Falls Quill in einer Version vorliegt, die diese Override-API
// nicht unterstützt, kippt der Editor nicht — wir loggen und fallen
// stillschweigend auf den nativen Image-Blot zurück.
if ( ! global . _ _ResizableImageRegistered ) {
try {
var Image = Quill . import ( 'formats/image' ) ;
var ATTRS = [ 'alt' , 'width' , 'height' ] ;
var ResizableImage = class extends Image {
static create ( value ) {
var src = value ;
var extras = null ;
if ( value && typeof value === 'object' ) {
src = value . src || value . url || '' ;
extras = value ;
}
var node = super . create ( src ) ;
if ( extras ) {
ATTRS . forEach ( function ( attr ) {
if ( extras [ attr ] !== undefined && extras [ attr ] !== null && extras [ attr ] !== '' ) {
node . setAttribute ( attr , extras [ attr ] ) ;
}
} ) ;
}
return node ;
}
static formats ( domNode ) {
var base = ( typeof super . formats === 'function' ) ? super . formats ( domNode ) : { } ;
ATTRS . forEach ( function ( attr ) {
if ( domNode . hasAttribute ( attr ) ) {
base [ attr ] = domNode . getAttribute ( attr ) ;
}
} ) ;
return base ;
}
format ( name , value ) {
if ( ATTRS . indexOf ( name ) > - 1 ) {
if ( value ) {
this . domNode . setAttribute ( name , value ) ;
} else {
this . domNode . removeAttribute ( name ) ;
}
} else {
super . format ( name , value ) ;
}
}
} ;
ResizableImage . blotName = 'image' ;
ResizableImage . tagName = 'IMG' ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
Quill . register ( ResizableImage , true ) ;
global . _ _ResizableImageRegistered = true ;
global . ResizableImage = ResizableImage ;
} catch ( err ) {
console . error ( '[quill_resize] ResizableImage-Blot konnte nicht registriert werden — fallback auf nativen Image-Blot. Resize-Größe geht beim Re-Edit verloren.' , err ) ;
}
}
var MIN _WIDTH = 50 ;
function ImageResize ( quill , options ) {
2026-06-22 11:49:59 +02:00
this . quill = quill ;
2026-06-23 10:00:25 +02:00
this . editor = quill . root ;
this . options = options || { } ;
2026-06-22 11:49:59 +02:00
this . activeImg = null ;
2026-06-23 10:00:25 +02:00
this . overlay = null ;
this . dragState = null ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
// Methoden binden, damit removeEventListener funktioniert
this . onEditorClick = this . onEditorClick . bind ( this ) ;
this . onDocPointerDown = this . onDocPointerDown . bind ( this ) ;
this . onReposition = this . onReposition . bind ( this ) ;
this . onMove = this . onMove . bind ( this ) ;
this . onUp = this . onUp . bind ( this ) ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
this . editor . addEventListener ( 'click' , this . onEditorClick ) ;
document . addEventListener ( 'mousedown' , this . onDocPointerDown , true ) ;
document . addEventListener ( 'touchstart' , this . onDocPointerDown , true ) ;
window . addEventListener ( 'resize' , this . onReposition ) ;
window . addEventListener ( 'scroll' , this . onReposition , true ) ;
// Bei Text-Changes Position anpassen (z.B. wenn Bild verschoben wird)
quill . on ( 'text-change' , this . onReposition ) ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
// Quill 2.0.0-dev.3 strippt beim dangerouslyPasteHTML alle <img>-Attribute
// außer src. Reader rendert das gespeicherte HTML direkt — also korrekt;
// aber im Editor ist die Größe nach Re-Open weg. Zwei Schutzschichten:
//
// (a) Clipboard-Matcher: Beim HTML→Delta-Parse die width/height/alt
// als delta.attributes mitführen. Bei korrekt greifendem
// ResizableImage-Blot überlebt das den Render zurück ans DOM.
//
// (b) Monkey-Patch um dangerouslyPasteHTML: Falls (a) in dev.3 nicht
// durchgereicht wird (Embed-format-Hook ignoriert), reapplizieren
// wir die Attribute nach dem Paste direkt am Editor-DOM. Idempotent
// pro Quill-Instanz via Marker-Property.
try {
quill . clipboard . addMatcher ( 'IMG' , function ( node , delta ) {
if ( ! node || ! delta || ! delta . ops || ! delta . ops . length ) return delta ;
var op = delta . ops [ 0 ] ;
if ( ! op || ! op . insert ) return delta ;
var attrs = op . attributes || { } ;
[ 'alt' , 'width' , 'height' ] . forEach ( function ( a ) {
if ( node . hasAttribute ( a ) && attrs [ a ] == null ) {
attrs [ a ] = node . getAttribute ( a ) ;
}
} ) ;
if ( Object . keys ( attrs ) . length ) op . attributes = attrs ;
return delta ;
} ) ;
} catch ( e ) {
console . warn ( '[quill_resize] Clipboard-Matcher Setup fehlgeschlagen' , e ) ;
2026-06-22 11:49:59 +02:00
}
2026-06-23 10:00:25 +02:00
if ( ! quill . _ _pasteHtmlPatched && quill . clipboard
&& typeof quill . clipboard . dangerouslyPasteHTML === 'function' ) {
var origPaste = quill . clipboard . dangerouslyPasteHTML ;
var PASTE _ATTRS = [ 'alt' , 'width' , 'height' ] ;
quill . clipboard . dangerouslyPasteHTML = function ( ) {
var args = Array . prototype . slice . call ( arguments ) ;
var htmlArg = ( typeof args [ 0 ] === 'string' ) ? args [ 0 ] : args [ 1 ] ;
var seq = [ ] ;
if ( typeof htmlArg === 'string' && htmlArg . indexOf ( '<img' ) !== - 1 ) {
var tmp = document . createElement ( 'div' ) ;
tmp . innerHTML = htmlArg ;
Array . prototype . forEach . call ( tmp . querySelectorAll ( 'img' ) , function ( img ) {
var entry = { src : img . getAttribute ( 'src' ) || '' , attrs : { } } ;
PASTE _ATTRS . forEach ( function ( a ) {
if ( img . hasAttribute ( a ) ) entry . attrs [ a ] = img . getAttribute ( a ) ;
} ) ;
seq . push ( entry ) ;
} ) ;
}
var result = origPaste . apply ( this , args ) ;
if ( seq . length ) {
var domImgs = quill . root . querySelectorAll ( 'img' ) ;
var ix = 0 ;
for ( var di = 0 ; di < domImgs . length && ix < seq . length ; di ++ ) {
var dimg = domImgs [ di ] ;
var dsrc = dimg . getAttribute ( 'src' ) || '' ;
while ( ix < seq . length && seq [ ix ] . src !== dsrc ) ix ++ ;
if ( ix < seq . length && seq [ ix ] . src === dsrc ) {
var entryAttrs = seq [ ix ] . attrs ;
Object . keys ( entryAttrs ) . forEach ( function ( k ) {
if ( ! dimg . hasAttribute ( k ) ) {
dimg . setAttribute ( k , entryAttrs [ k ] ) ;
}
} ) ;
// Inline-Style mit !important: das HTML-width-Attribut
// hat Spezifität 0 und wird im Intranet von der globalen
// mysyde.css-img-Regel überstimmt (oder von Quills internem
// Embed-Wrapping). Inline-!important gewinnt garantiert.
// Spiegelt das Verhalten von onMove (live-drag) wider.
if ( entryAttrs . width ) {
dimg . style . setProperty ( 'width' , entryAttrs . width + 'px' , 'important' ) ;
dimg . style . setProperty ( 'height' , 'auto' , 'important' ) ;
}
ix ++ ;
}
}
}
return result ;
} ;
quill . _ _pasteHtmlPatched = true ;
}
}
ImageResize . prototype . onEditorClick = function ( e ) {
if ( e . target && e . target . tagName === 'IMG' && this . editor . contains ( e . target ) ) {
this . attachTo ( e . target ) ;
} else {
this . detach ( ) ;
2026-06-22 11:49:59 +02:00
}
} ;
2026-06-23 10:00:25 +02:00
ImageResize . prototype . attachTo = function ( img ) {
if ( this . activeImg === img && this . overlay ) return ;
this . detach ( ) ;
2026-06-22 11:49:59 +02:00
this . activeImg = img ;
2026-06-23 10:00:25 +02:00
this . overlay = this . buildOverlay ( ) ;
document . body . appendChild ( this . overlay ) ;
this . reposition ( ) ;
} ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
ImageResize . prototype . buildOverlay = function ( ) {
var self = this ;
var box = document . createElement ( 'div' ) ;
box . className = 'ql-image-overlay' ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
[ 'nw' , 'ne' , 'sw' , 'se' ] . forEach ( function ( corner ) {
var h = document . createElement ( 'div' ) ;
h . className = 'ql-image-handle ql-image-handle-' + corner ;
h . dataset . corner = corner ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
// Corner explizit über Closure binden, nicht über e.currentTarget
// (currentTarget kann nach Listener-Return null sein, je nach Browser).
var startDrag = function ( e ) { self . onHandleDown ( e , corner ) ; } ;
h . addEventListener ( 'mousedown' , startDrag ) ;
h . addEventListener ( 'touchstart' , startDrag , { passive : false } ) ;
2026-06-22 11:49:59 +02:00
2026-06-23 10:00:25 +02:00
box . appendChild ( h ) ;
2026-06-22 11:49:59 +02:00
} ) ;
2026-06-23 10:00:25 +02:00
return box ;
2026-06-22 11:49:59 +02:00
} ;
2026-06-23 10:00:25 +02:00
ImageResize . prototype . reposition = function ( ) {
if ( ! this . activeImg || ! this . overlay ) return ;
2026-06-22 11:49:59 +02:00
var r = this . activeImg . getBoundingClientRect ( ) ;
2026-06-23 10:00:25 +02:00
var s = this . overlay . style ;
s . position = 'fixed' ;
s . left = r . left + 'px' ;
s . top = r . top + 'px' ;
s . width = r . width + 'px' ;
s . height = r . height + 'px' ;
// KEIN pointer-events:none hier — die Handles brauchen Klick-Empfang.
// Der gestrichelte Rand des Overlays liegt im CSS auf
// `.ql-image-overlay` mit pointer-events:none direkt im CSS.
s . zIndex = '10000' ;
} ;
ImageResize . prototype . onReposition = function ( ) {
this . reposition ( ) ;
2026-06-22 11:49:59 +02:00
} ;
2026-06-23 10:00:25 +02:00
ImageResize . prototype . onHandleDown = function ( e , corner ) {
2026-06-22 11:49:59 +02:00
if ( ! this . activeImg ) return ;
2026-06-23 10:00:25 +02:00
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
var p = ( e . touches && e . touches [ 0 ] ) ? e . touches [ 0 ] : e ;
var r = this . activeImg . getBoundingClientRect ( ) ;
this . dragState = {
startX : p . clientX ,
startY : p . clientY ,
startW : r . width ,
startH : r . height ,
corner : corner
} ;
document . addEventListener ( 'mousemove' , this . onMove ) ;
document . addEventListener ( 'mouseup' , this . onUp ) ;
document . addEventListener ( 'touchmove' , this . onMove , { passive : false } ) ;
document . addEventListener ( 'touchend' , this . onUp ) ;
document . addEventListener ( 'touchcancel' , this . onUp ) ;
} ;
ImageResize . prototype . onMove = function ( e ) {
if ( ! this . dragState || ! this . activeImg ) return ;
if ( e . cancelable ) e . preventDefault ( ) ;
var p = ( e . touches && e . touches [ 0 ] ) ? e . touches [ 0 ] : e ;
var dx = p . clientX - this . dragState . startX ;
// Links liegende Handles invertieren das Vorzeichen:
// rechts ziehen → dx positiv → image schrumpft (left handle)
if ( this . dragState . corner === 'nw' || this . dragState . corner === 'sw' ) {
dx = - dx ;
}
var editorR = this . editor . getBoundingClientRect ( ) ;
var maxW = Math . max ( MIN _WIDTH , editorR . width - 8 ) ;
var newW = Math . max ( MIN _WIDTH , Math . min ( maxW , this . dragState . startW + dx ) ) ;
newW = Math . round ( newW ) ;
// width sowohl als HTML-Attribut als auch als Inline-Style setzen.
// Inline-Style ist nötig, weil im Intranet eine CSS-Regel mit höherer
// Spezifität als das Attribut existiert (an dieser Stelle nicht zu
// finden ohne Live-DevTools). Inline-Spezifität (1000) gewinnt gegen
// alle nicht-!important CSS-Regeln. setProperty mit 'important' als
// Fallback, falls eine !important-Regel im Spiel ist.
var widthPx = newW + 'px' ;
this . activeImg . setAttribute ( 'width' , String ( newW ) ) ;
this . activeImg . style . setProperty ( 'width' , widthPx , 'important' ) ;
this . activeImg . style . setProperty ( 'height' , 'auto' , 'important' ) ;
this . activeImg . removeAttribute ( 'height' ) ;
this . reposition ( ) ;
2026-06-22 11:49:59 +02:00
} ;
2026-06-23 10:00:25 +02:00
ImageResize . prototype . onUp = function ( ) {
document . removeEventListener ( 'mousemove' , this . onMove ) ;
document . removeEventListener ( 'mouseup' , this . onUp ) ;
document . removeEventListener ( 'touchmove' , this . onMove ) ;
document . removeEventListener ( 'touchend' , this . onUp ) ;
document . removeEventListener ( 'touchcancel' , this . onUp ) ;
if ( this . dragState ) {
this . dragState = null ;
try { this . quill . update ( 'user' ) ; } catch ( e ) { }
}
} ;
ImageResize . prototype . onDocPointerDown = function ( e ) {
if ( ! this . activeImg ) return ;
if ( this . overlay && this . overlay . contains ( e . target ) ) return ;
if ( e . target === this . activeImg ) return ;
this . detach ( ) ;
2026-06-22 11:49:59 +02:00
} ;
2026-06-23 10:00:25 +02:00
ImageResize . prototype . detach = function ( ) {
2026-06-22 11:49:59 +02:00
if ( this . overlay && this . overlay . parentNode ) {
this . overlay . parentNode . removeChild ( this . overlay ) ;
}
this . overlay = null ;
this . activeImg = null ;
2026-06-23 10:00:25 +02:00
this . dragState = null ;
2026-06-22 11:49:59 +02:00
} ;
2026-06-23 10:00:25 +02:00
Quill . register ( 'modules/imageResize' , ImageResize ) ;
global . QuillImageResize = ImageResize ;
} ) ( window ) ;