From 54d9194d2292d7703986cab364aeedba6542be18 Mon Sep 17 00:00:00 2001 From: Moritz Weinmann Date: Tue, 23 Jun 2026 10:00:25 +0200 Subject: [PATCH] =?UTF-8?q?refactor(quill):=20Toolbar-Presets=20als=20Fact?= =?UTF-8?q?ory-Funktionen,=20Resize-Plugin=20=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toolbar-Presets liefern jetzt frische Arrays (Factory-Pattern), damit mehrere Editor-Instanzen auf einer Seite sich nicht gegenseitig beeinflussen. Resize-Modul und CSS angepasst. Co-Authored-By: Claude Sonnet 4.6 --- .../shared_components/quill/quill_resize.css | 65 ++- .../shared_components/quill/quill_resize.js | 403 +++++++++++++++--- .../quill/toolbar_presets.js | 146 +++++-- 3 files changed, 491 insertions(+), 123 deletions(-) diff --git a/module/shared_components/quill/quill_resize.css b/module/shared_components/quill/quill_resize.css index ba03566..50e3612 100644 --- a/module/shared_components/quill/quill_resize.css +++ b/module/shared_components/quill/quill_resize.css @@ -1,20 +1,59 @@ -.ql-img-resize-overlay { - position: absolute; - border: 1px dashed #3C7DD9; +/* Drag-Handle Overlay für Quill Image Resize */ + +.ql-image-overlay { box-sizing: border-box; + border: 1px dashed #2684ff; pointer-events: none; - z-index: 100; } -.ql-img-resize-handle { +.ql-image-handle { position: absolute; - bottom: -5px; - right: -5px; - width: 10px; - height: 10px; - background: #3C7DD9; - border: 1px solid #fff; + width: 12px; + height: 12px; + background: #ffffff; + border: 2px solid #2684ff; border-radius: 2px; - cursor: nwse-resize; - pointer-events: all; + pointer-events: auto; + z-index: 10001; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.3); } + +.ql-image-handle:hover { + background: #2684ff; +} + +.ql-image-handle-nw { + left: -6px; + top: -6px; + cursor: nwse-resize; +} + +.ql-image-handle-ne { + right: -6px; + top: -6px; + cursor: nesw-resize; +} + +.ql-image-handle-sw { + left: -6px; + bottom: -6px; + cursor: nesw-resize; +} + +.ql-image-handle-se { + right: -6px; + bottom: -6px; + cursor: nwse-resize; +} + +/* Während des Drags Text-Selection global unterdrücken */ +.ql-image-overlay.is-dragging, +.ql-image-overlay.is-dragging * { + user-select: none; + -webkit-user-select: none; +} + +/* Im Editor selbst soll das Bild visuell ein leichtes Outline bekommen, + wenn es aktiv ist — aber CSS kann nicht aus dem Overlay zurückgreifen, + daher: ein dünner Schatten ist genug; der gestrichelte Rand am Overlay + markiert das aktive Bild klar genug. */ diff --git a/module/shared_components/quill/quill_resize.js b/module/shared_components/quill/quill_resize.js index a1c05d9..a691a95 100644 --- a/module/shared_components/quill/quill_resize.js +++ b/module/shared_components/quill/quill_resize.js @@ -1,96 +1,363 @@ -(function () { +/** + * Quill-Modul: Drag-Resize für Bilder im Editor. + * + * Aktivierung pro Editor-Instanz via: + * modules: { imageResize: true } + * + * Verhalten: + * - Click auf 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 + * (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 ) + * - 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) { 'use strict'; - if (typeof Quill === 'undefined') return; - - function ImageResizeModule(quill, options) { - this.quill = quill; - this.overlay = null; - this.activeImg = null; - this._startX = 0; - this._startW = 0; - - this._onImgClick = this._onImgClick.bind(this); - this._onDocClick = this._onDocClick.bind(this); - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); - - quill.root.addEventListener('click', this._onImgClick); - document.addEventListener('click', this._onDocClick); + if (typeof Quill === 'undefined') { + console.error('[quill_resize] Quill ist nicht geladen.'); + return; } - ImageResizeModule.prototype._onImgClick = function (e) { - if (e.target && e.target.tagName === 'IMG') { - e.stopPropagation(); - this._select(e.target); + // 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'; + + 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) { + this.quill = quill; + this.editor = quill.root; + this.options = options || {}; + this.activeImg = null; + this.overlay = null; + this.dragState = null; + + // 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); + + 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); + + // Quill 2.0.0-dev.3 strippt beim dangerouslyPasteHTML alle -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); + } + + 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(' -1; }); + // if (idx > -1) t[idx] = ['code-block']; + // return t; + // }, + // + // minimal: function () { + // return [ + // [{ header: [2, 3, false] }], + // ['bold', 'italic', 'underline'], + // [{ list: 'ordered' }, { list: 'bullet' }], + // ['link'], + // ['clean'] + // ]; + // } + }; +})(window);