.. _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 Schema Definition ================= .. 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', 'linkHref', 'linkTarget', 'linkTitle' ], }); 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); 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' ] 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`