.. include:: /Includes.rst.txt .. _ckeditor-model-element: ================================== CKEditor Model Element Reference ================================== Complete reference for the ``typo3image`` custom model element in CKEditor 5. Overview ======== The ``typo3image`` is a custom model element that represents TYPO3 FAL-integrated images in the CKEditor document model. It extends CKEditor's base ``$blockObject`` and includes TYPO3-specific attributes for FAL integration, image processing, and metadata management. **File**: ``Resources/Public/JavaScript/Plugins/typo3image.js`` Model vs View Architecture =========================== Understanding CKEditor 5 Architecture -------------------------------------- CKEditor 5 uses a Model-View-Controller (MVC) architecture:: ┌─────────────────────────────────────────────────┐ │ Model Layer (Data) │ │ - Abstract representation of document │ │ - Business logic and validation │ │ - typo3image element with attributes │ └──────────────────┬──────────────────────────────┘ │ │ Conversions │ ┌──────────────────▼──────────────────────────────┐ │ View Layer (DOM) │ │ - Visual representation in editor │ │ - elements with HTML attributes │ │ - User sees and interacts with │ └──────────────────┬──────────────────────────────┘ │ │ Rendering │ ┌──────────────────▼──────────────────────────────┐ │ DOM (Browser) │ │ - Actual HTML in contenteditable │ │ - │ └─────────────────────────────────────────────────┘ Why Separate Model and View? ----------------------------- - **Data Integrity**: Model maintains clean data structure regardless of DOM quirks - **Cross-Platform**: Same model can render differently on different platforms - **Collaboration**: Multiple users can edit same model with conflict resolution - **Undo/Redo**: Model changes tracked for history management - **Validation**: Business rules enforced in model layer Model Elements ============== The extension provides two model elements for different use cases: - **typo3image**: Block-level images wrapped in ``
`` (with optional caption) - **typo3imageInline**: True inline images that flow with text Block Image Schema (typo3image) ------------------------------- .. code-block:: javascript editor.model.schema.register('typo3image', { inheritAllFrom: '$blockObject', allowIn: ['$text', '$block'], allowAttributes: [ 'src', 'fileUid', 'fileTable', 'alt', 'altOverride', 'title', 'titleOverride', 'class', 'enableZoom', 'width', 'height', 'htmlA', 'imageLinkHref', 'imageLinkTarget', 'imageLinkTitle', 'imageLinkClass', 'imageLinkParams' ], }); Inline Image Schema (typo3imageInline) -------------------------------------- .. versionadded:: 13.6 True inline images with cursor positioning support. .. code-block:: javascript editor.model.schema.register('typo3imageInline', { inheritAllFrom: '$inlineObject', allowIn: ['$block'], allowAttributes: [ 'src', 'fileUid', 'fileTable', 'alt', 'altOverride', 'title', 'titleOverride', 'class', 'enableZoom', 'width', 'height', 'noScale', 'quality', 'imageLinkHref', 'imageLinkTarget', 'imageLinkTitle', 'imageLinkClass', 'imageLinkParams' ], }); Key Differences ^^^^^^^^^^^^^^^ .. list-table:: :header-rows: 1 :widths: 30 35 35 * - Feature - typo3image (Block) - typo3imageInline (Inline) * - Inheritance - ``$blockObject`` - ``$inlineObject`` * - Caption Support - ✅ Yes (typo3imageCaption child) - ❌ No * - Text Flow - Breaks paragraph (block-level) - Flows with text (inline) * - Cursor Position - Before/after figure - Before/after on same line * - Output HTML - ``
`` - ```` * - Style Classes - ``image-left``, ``image-right``, ``image-center``, ``image-block`` - ``image-inline`` Usage Example ^^^^^^^^^^^^^ Text can flow around inline images: |inline-example| .. |inline-example| image:: /Images/inline-image-example.png :height: 20px In the editor, users can type text before and after inline images on the same line, just like typing around any other inline element (bold text, links, etc.). Toggle Command ^^^^^^^^^^^^^^ Users can convert between block and inline via the ``toggleImageType`` command: .. code-block:: javascript // Toggle current image between block and inline editor.execute('toggleImageType'); // Check current type const isInline = editor.commands.get('toggleImageType').value === 'inline'; **Block → Inline Conversion:** - Caption is removed (inline images cannot have captions) - Block style classes removed, ``image-inline`` added - Image becomes inline in text flow **Inline → Block Conversion:** - Image wrapped in figure - ``image-block`` class added (or previous alignment class) - Image becomes block-level element Schema Properties Explained ---------------------------- inheritAllFrom: '$blockObject' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inherits all properties from CKEditor's base ``$blockObject``: - **Selectable**: Can be selected like any block element - **Object**: Treated as atomic unit (not text content) - **Focusable**: Can receive focus for editing - **Non-Breaking**: Cannot be split by Enter key allowIn: ['$text', '$block'] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Defines where ``typo3image`` can exist: - **$text**: Inside text content (inline-like behavior) - **$block**: Inside block elements (paragraphs, divs, etc.) **Result**: Images can be placed in any text flow or block context. allowAttributes: [...] ^^^^^^^^^^^^^^^^^^^^^^ Lists all valid attributes for the model element. Attributes not listed are stripped. Attribute Reference =================== Core Attributes --------------- src ^^^ .. code-block:: javascript type: String required: true **Purpose**: Image source URL (absolute or relative) **Examples**: .. code-block:: javascript src: '/fileadmin/user_upload/image.jpg' src: '/fileadmin/_processed_/a/b/csm_image_123.jpg' src: 'https://example.com/external.jpg' **Usage**: .. code-block:: javascript const src = modelElement.getAttribute('src'); writer.setAttribute('src', '/new/path.jpg', modelElement); fileUid ^^^^^^^ .. code-block:: javascript type: Number required: true (for TYPO3 FAL integration) **Purpose**: TYPO3 File Abstraction Layer file UID **Range**: Positive integer matching ``sys_file.uid`` **Example**: .. code-block:: javascript fileUid: 123 **Usage**: .. code-block:: javascript const fileUid = modelElement.getAttribute('fileUid'); writer.setAttribute('fileUid', 456, modelElement); **Backend Integration**: .. code-block:: javascript // Fetch file info from backend const fileUid = modelElement.getAttribute('fileUid'); const fileInfo = await fetch( routeUrl + '&action=info&fileId=' + fileUid ).then(r => r.json()); fileTable ^^^^^^^^^ .. code-block:: javascript type: String default: 'sys_file' **Purpose**: Database table name for file reference **Valid Values**: ``'sys_file'`` (default), ``'sys_file_reference'`` (rarely used) **Example**: .. code-block:: javascript fileTable: 'sys_file' **Usage**: .. code-block:: javascript const table = modelElement.getAttribute('fileTable') || 'sys_file'; Metadata Attributes ------------------- alt ^^^ .. code-block:: javascript type: String default: '' **Purpose**: Alternative text for accessibility (WCAG compliance) **Recommendations**: - Describe image content concisely - Required for accessibility - Max ~125 characters for optimal screen reader experience **Example**: .. code-block:: javascript alt: 'Product photo showing red widget from front angle' **Usage**: .. code-block:: javascript writer.setAttribute('alt', 'New alt text', modelElement); altOverride ^^^^^^^^^^^ .. code-block:: javascript type: Boolean default: false **Purpose**: Flag indicating alt text was manually overridden by user **Behavior**: - ``false``: Use alt from FAL file metadata - ``true``: Use custom alt text from ``alt`` attribute **Example**: .. code-block:: javascript altOverride: true // Custom alt text takes precedence **Usage Pattern**: .. code-block:: javascript // In image dialog if (customAltCheckbox.checked) { writer.setAttribute('alt', customAltValue, modelElement); writer.setAttribute('altOverride', true, modelElement); } else { writer.removeAttribute('alt', modelElement); writer.removeAttribute('altOverride', modelElement); } title ^^^^^ .. code-block:: javascript type: String default: '' **Purpose**: Advisory title (tooltip text shown on hover) **Recommendations**: - Optional supplementary information - Not a replacement for alt text - Brief contextual information **Example**: .. code-block:: javascript title: 'Click to view full size' **Usage**: .. code-block:: javascript writer.setAttribute('title', 'Tooltip text', modelElement); titleOverride ^^^^^^^^^^^^^ .. code-block:: javascript type: Boolean default: false **Purpose**: Flag indicating title was manually overridden by user **Behavior**: - ``false``: Use title from FAL file metadata - ``true``: Use custom title from ``title`` attribute **Example**: .. code-block:: javascript titleOverride: true Visual Attributes ----------------- class ^^^^^ .. code-block:: javascript type: String default: '' **Purpose**: Space-separated CSS class names for styling **Style Integration**: Modified by CKEditor style system via ``GeneralHtmlSupport`` **Examples**: .. code-block:: javascript class: 'float-left img-responsive' class: 'img-thumbnail d-block mx-auto' **Usage**: .. code-block:: javascript // Manual class setting writer.setAttribute('class', 'my-class another-class', modelElement); // Style system automatically updates this attribute // when user selects a style from dropdown **Style System Integration**: .. code-block:: javascript // CKEditor style definition { name: 'Image Left', element: 'img', classes: ['float-left', 'mr-3'] } // Results in: class: 'float-left mr-3' width ^^^^^ .. code-block:: javascript type: String (pixels without unit) default: '' **Purpose**: Image display width in pixels **Format**: Numeric string without 'px' unit **Examples**: .. code-block:: javascript width: '800' width: '1200' **Usage**: .. code-block:: javascript writer.setAttribute('width', '800', modelElement); **Aspect Ratio Preservation**: .. code-block:: javascript // When width changes, height should be recalculated const newWidth = 800; const originalWidth = img.width; const originalHeight = img.height; const ratio = originalWidth / originalHeight; const newHeight = Math.ceil(newWidth / ratio); writer.setAttribute('width', String(newWidth), modelElement); writer.setAttribute('height', String(newHeight), modelElement); height ^^^^^^ .. code-block:: javascript type: String (pixels without unit) default: '' **Purpose**: Image display height in pixels **Format**: Numeric string without 'px' unit **Examples**: .. code-block:: javascript height: '600' height: '900' **Usage**: .. code-block:: javascript writer.setAttribute('height', '600', modelElement); enableZoom ^^^^^^^^^^ .. code-block:: javascript type: Boolean default: false **Purpose**: Enable zoom/click-to-enlarge functionality (TYPO3-specific feature) **Behavior**: - ``true``: Image becomes clickable, opens larger version - ``false``: Image is static, no click interaction **Example**: .. code-block:: javascript enableZoom: true **Frontend Rendering**: .. code-block:: html **Usage**: .. code-block:: javascript writer.setAttribute('enableZoom', true, modelElement); Link Attributes --------------- htmlA ^^^^^ .. code-block:: javascript type: String default: undefined **Purpose**: Complete HTML anchor tag wrapping the image (legacy attribute) **Format**: Full HTML string **Example**: .. code-block:: javascript htmlA: '...' .. note:: This is a legacy attribute. Modern approach uses separate link attributes. linkHref ^^^^^^^^ .. code-block:: javascript type: String default: undefined **Purpose**: Link URL when image is wrapped in anchor tag **Examples**: .. code-block:: javascript linkHref: '/page/detail' linkHref: 'https://example.com' linkHref: 'mailto:info@example.com' **Usage**: .. code-block:: javascript writer.setAttribute('linkHref', '/page/123', modelElement); linkTarget ^^^^^^^^^^ .. code-block:: javascript type: String default: undefined **Purpose**: Link target attribute (_blank, _self, _parent, _top) **Valid Values**: ``'_blank'``, ``'_self'``, ``'_parent'``, ``'_top'``, or named frame **Example**: .. code-block:: javascript linkTarget: '_blank' **Usage**: .. code-block:: javascript writer.setAttribute('linkTarget', '_blank', modelElement); linkTitle ^^^^^^^^^ .. code-block:: javascript type: String default: undefined **Purpose**: Title attribute for the wrapping anchor tag **Example**: .. code-block:: javascript linkTitle: 'Click to view product details' **Usage**: .. code-block:: javascript writer.setAttribute('linkTitle', 'Link description', modelElement); linkClass ^^^^^^^^^ .. versionadded:: 13.5.0 .. code-block:: javascript type: String default: undefined **Purpose**: CSS class for the wrapping anchor tag **Example**: .. code-block:: javascript linkClass: 'external-link btn-primary' **Usage**: .. code-block:: javascript writer.setAttribute('linkClass', 'my-link-class', modelElement); linkParams ^^^^^^^^^^ .. versionadded:: 13.5.0 .. code-block:: javascript type: String default: undefined **Purpose**: Additional URL parameters for the link (TypoLink additionalParams) **Format**: Query string starting with ``&`` (e.g., ``&L=1&type=123``) **Example**: .. code-block:: javascript linkParams: '&L=1&type=123' **Usage**: .. code-block:: javascript writer.setAttribute('linkParams', '&cHash=abc123', modelElement); **TypoLink Integration**: This corresponds to the fifth parameter in TYPO3's TypoLink format:: url target class "title" additionalParams ↓ ↓ linkHref linkParams Working with Model Elements ============================ Creating Model Elements ----------------------- .. code-block:: javascript editor.model.change(writer => { const typo3image = writer.createElement('typo3image', { src: '/fileadmin/image.jpg', fileUid: 123, fileTable: 'sys_file', width: '800', height: '600', alt: 'Description', class: 'img-fluid' }); // Insert at current selection const insertPosition = editor.model.document.selection.getFirstPosition(); editor.model.insertContent(typo3image, insertPosition); }); Updating Attributes ------------------- .. code-block:: javascript editor.model.change(writer => { const selectedElement = editor.model.document.selection.getSelectedElement(); if (selectedElement && selectedElement.name === 'typo3image') { // Update single attribute writer.setAttribute('width', '1200', selectedElement); // Update multiple attributes writer.setAttributes({ width: '1200', height: '800', class: 'img-large' }, selectedElement); } }); Reading Attributes ------------------ .. code-block:: javascript const selectedElement = editor.model.document.selection.getSelectedElement(); if (selectedElement && selectedElement.name === 'typo3image') { // Read single attribute const src = selectedElement.getAttribute('src'); const fileUid = selectedElement.getAttribute('fileUid'); // Read with default fallback const alt = selectedElement.getAttribute('alt') || ''; const width = selectedElement.getAttribute('width') || '0'; // Check if attribute exists const hasClass = selectedElement.hasAttribute('class'); // Get all attributes const allAttrs = Array.from(selectedElement.getAttributes()); console.log(allAttrs); // [['src', '...'], ['fileUid', 123], ...] } Removing Attributes ------------------- .. code-block:: javascript editor.model.change(writer => { const selectedElement = editor.model.document.selection.getSelectedElement(); if (selectedElement && selectedElement.name === 'typo3image') { // Remove single attribute writer.removeAttribute('class', selectedElement); // Remove multiple attributes writer.removeAttribute('title', selectedElement); writer.removeAttribute('titleOverride', selectedElement); } }); Model Selection =============== Selecting Elements ------------------ .. code-block:: javascript // Select element programmatically editor.model.change(writer => { const element = /* get element reference */; writer.setSelection(element, 'on'); // 'on' = select element itself }); Getting Selected Element ------------------------- .. code-block:: javascript const selection = editor.model.document.selection; const selectedElement = selection.getSelectedElement(); if (selectedElement && selectedElement.name === 'typo3image') { // Image is selected console.log('Selected typo3image element'); } Iterating Selection Range -------------------------- .. code-block:: javascript const selection = editor.model.document.selection; const range = selection.getFirstRange(); for (const item of range.getItems()) { if (item.is('element', 'typo3image')) { console.log('Found typo3image:', item.getAttribute('src')); } } Model Traversal =============== Finding Parent Elements ----------------------- .. code-block:: javascript const element = /* typo3image element */; const parent = element.parent; console.log(parent.name); // e.g., 'paragraph', '$root' Finding Previous/Next Siblings ------------------------------- .. code-block:: javascript const element = /* typo3image element */; const previousSibling = element.previousSibling; const nextSibling = element.nextSibling; Walking the Model Tree ---------------------- .. code-block:: javascript function findAllImages(root) { const images = []; const walker = editor.model.createRangeIn(root).getWalker(); for (const {item} of walker) { if (item.is('element', 'typo3image')) { images.push(item); } } return images; } // Find all images in document const allImages = findAllImages(editor.model.document.getRoot()); Attribute Validation ==================== Allowed Attributes Enforcement ------------------------------- CKEditor automatically strips attributes not in ``allowAttributes`` list: .. code-block:: javascript // This attribute will be stripped writer.setAttribute('invalidAttr', 'value', modelElement); // Only these attributes are preserved allowAttributes: [ 'src', 'fileUid', 'fileTable', 'alt', 'altOverride', 'title', 'titleOverride', 'class', 'enableZoom', 'width', 'height', 'htmlA', 'linkHref', 'linkTarget', 'linkTitle', 'linkClass', 'linkParams' ] Custom Validation ----------------- .. code-block:: javascript editor.model.schema.addAttributeCheck((context, attributeName) => { // Only allow width/height with valid numeric values if (attributeName === 'width' || attributeName === 'height') { if (context.endsWith('typo3image')) { const value = context.getAttribute(attributeName); return /^\d+$/.test(value); // Must be numeric } } return true; }); Model Change Listeners ====================== Listening to Attribute Changes ------------------------------- .. code-block:: javascript editor.model.document.on('change:data', () => { const changes = editor.model.document.differ.getChanges(); for (const change of changes) { if (change.type === 'attribute' && change.attributeKey === 'class') { console.log('Class changed:', { element: change.range.start.parent.name, oldValue: change.attributeOldValue, newValue: change.attributeNewValue }); } } }); Listening to Element Insertion ------------------------------- .. code-block:: javascript editor.model.document.on('change:data', () => { const changes = editor.model.document.differ.getChanges(); for (const change of changes) { if (change.type === 'insert' && change.name === 'typo3image') { console.log('typo3image inserted:', change.position.path); } } }); Advanced Patterns ================= Cloning Elements ---------------- .. code-block:: javascript editor.model.change(writer => { const original = /* get typo3image element */; // Clone with all attributes const clone = writer.cloneElement(original); // Insert clone const insertPosition = /* target position */; writer.insert(clone, insertPosition); }); Batch Attribute Updates ----------------------- .. code-block:: javascript editor.model.change(writer => { const images = /* array of typo3image elements */; // Apply same class to all images images.forEach(img => { const currentClass = img.getAttribute('class') || ''; const newClass = currentClass + ' batch-processed'; writer.setAttribute('class', newClass.trim(), img); }); }); Conditional Attribute Setting ------------------------------ .. code-block:: javascript editor.model.change(writer => { const element = /* typo3image element */; // Only set width if not already set if (!element.hasAttribute('width')) { writer.setAttribute('width', '800', element); } // Update alt only if override is enabled if (element.getAttribute('altOverride')) { writer.setAttribute('alt', customAltText, element); } }); Debugging Model Elements ======================== Inspect Element in Console --------------------------- .. code-block:: javascript // Get selected element const element = editor.model.document.selection.getSelectedElement(); // Log all attributes console.log('Element:', element.name); console.log('Attributes:', Array.from(element.getAttributes())); // Log specific attributes console.log('src:', element.getAttribute('src')); console.log('fileUid:', element.getAttribute('fileUid')); console.log('class:', element.getAttribute('class')); Monitor Model Changes --------------------- .. code-block:: javascript editor.model.document.on('change', (evt, batch) => { console.log('Model changed, batch type:', batch.type); console.log('Is undoable:', batch.isUndoable); const changes = editor.model.document.differ.getChanges(); console.log('Changes:', changes); }); Visualize Model Structure -------------------------- .. code-block:: javascript function logModelTree(element, indent = 0) { const prefix = ' '.repeat(indent); if (element.is('$text')) { console.log(prefix + 'TEXT:', element.data); } else { console.log(prefix + element.name); for (const child of element.getChildren()) { logModelTree(child, indent + 2); } } } // Log entire document structure logModelTree(editor.model.document.getRoot()); Related Documentation ===================== - :ref:`ckeditor-plugin-development-guide` - :ref:`ckeditor-style-integration` - :ref:`ckeditor-conversions` - :ref:`architecture-overview`