Files
avives 1f72193a64
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
2026-08-21 19:29:00 -06:00

18 KiB

Monaco Code Editor for Winter CMS

This is the Monaco Editor integration for Winter CMS Backend, replacing the legacy Ace Editor with Microsoft's Monaco Editor (the same editor that powers VS Code).

Overview

Monaco Editor provides a rich, modern code editing experience with:

  • IntelliSense (code completion)
  • Syntax highlighting for 15+ languages
  • Advanced find/replace with regex support
  • Multi-cursor editing
  • Code folding
  • Bracket matching and colorization
  • Minimap overview
  • Color picker for CSS colors
  • And many more VS Code features

Features

Supported Languages (15)

  1. TypeScript - Full TypeScript support with type checking
  2. JavaScript - Modern ES6+ support
  3. CSS - Including CSS3 properties
  4. JSON - With schema validation
  5. HTML - HTML5 support
  6. INI - Configuration files
  7. LESS - CSS preprocessor
  8. Markdown - Rich markdown editing
  9. MySQL - SQL syntax highlighting
  10. PHP - Full PHP support
  11. SCSS - Sass CSS preprocessor
  12. Twig - Template engine syntax
  13. XML - Markup language support
  14. YAML - Configuration file support

Monaco Features (20+)

Enabled features include:

  • Anchor select
  • Bracket matching
  • Caret operations
  • Clipboard operations
  • Code lens
  • Color picker
  • Comment toggling
  • Context menu
  • Cursor undo/redo
  • Find and replace
  • Code folding
  • Go to symbol
  • Hover information
  • In-place replace
  • Indentation
  • Inline hints
  • Links
  • Multi-cursor editing
  • Parameter hints
  • Rename symbol
  • Smart select
  • Snippets
  • Suggest (autocomplete)
  • Word highlighter
  • Word operations

Themes (35+)

Includes legacy tmTheme themes plus modern JSON themes

User Preferences

All editor preferences are configurable from Backend → Preferences → Code editor:

Appearance:

  • Font size (default: 12px)
  • Theme selection
  • Show/hide line numbers (gutter)
  • Show/hide invisibles (whitespace)
  • Highlight active line
  • Show minimap
  • Bracket colorization
  • Color picker for CSS

Behavior:

  • Tab size (default: 4 spaces)
  • Use soft tabs (spaces) vs hard tabs
  • Word wrap
  • Auto-closing brackets/quotes
  • Code folding
  • Indent guides
  • Print margin

All preferences persist across sessions and are stored per-user.

Editor Architecture

Winter CMS uses a dual-editor architecture to optimize for different use cases:

Monaco Editor (this FormWidget)

Used by: CodeEditor FormWidget Location: /modules/backend/formwidgets/codeeditor/ Purpose: Advanced code editing with IntelliSense, syntax highlighting, and modern IDE features Bundle Size: ~15 MB gzipped (main bundle + workers) Best for: Writing PHP, JavaScript, CSS, YAML, and other code files

Ace Editor (preserved)

Used by: RichEditor and MarkdownEditor FormWidgets Location: /modules/backend/assets/vendor/ace-codeeditor/ Purpose: HTML source code editing within WYSIWYG editors Bundle Size: ~500 KB (significantly lighter) Best for: Viewing/editing raw HTML in rich text contexts

Why Both?

Monaco for CodeEditor:

  • Full IntelliSense and code completion
  • Advanced refactoring tools
  • Multi-cursor editing
  • Rich language support
  • Worth the bundle size for dedicated code editing

Ace for RichEditor/MarkdownEditor:

  • Users rarely need advanced IDE features for HTML source view
  • Lighter bundle improves page load performance
  • Sufficient for basic HTML editing needs
  • Reduces total application bundle by keeping WYSIWYG tools lean

This architecture balances modern features where they matter most (code editing) with performance optimization for general-purpose rich text editing.

Technical Details

Architecture

modules/backend/formwidgets/codeeditor/
├── assets/
│   ├── css/
│   │   └── codeeditor.css - Compiled styles
│   ├── fonts/
│   │   └── codicon.ttf - Monaco icons font
│   ├── js/
│   │   ├── codeeditor.js - Main Monaco integration
│   │   └── build/
│   │       ├── codeeditor.bundle.js - Main bundle (19 MB)
│   │       ├── css.worker.js - CSS language worker
│   │       ├── editor.worker.js - Base editor worker
│   │       ├── html.worker.js - HTML language worker
│   │       ├── json.worker.js - JSON language worker
│   │       ├── ts.worker.js - TypeScript worker
│   │       └── [language-chunks] - 15 language modules
│   ├── less/
│   │   └── codeeditor.less - Source styles
│   ├── themes/
│   │   ├── [34 .tmTheme files] - Legacy TextMate themes
│   │   ├── one-dark-pro.json - Modern JSON theme
│   │   └── winter.json - Modern JSON theme
│   ├── winter.mix.js - Laravel Mix build configuration
│   └── package.json - NPM dependencies (in parent)
├── partials/
│   └── codeeditor.htm - Widget template
└── CodeEditor.php - FormWidget class

Build System

Current: Laravel Mix 6 with Webpack 5

Build Command

php artisan mix:compile --package=module-backend.formwidgets.codeeditor -f

Build Configuration

See assets/winter.mix.js:

  • Uses monaco-editor-webpack-plugin for proper worker splitting
  • Polyfills for browser compatibility (> 0.5%, last 2 versions, Firefox ESR)
  • Removes inline codicon font CSS (post-build hook)
  • Minification and terser optimization

Web Workers

Monaco Editor uses Web Workers for language services:

Worker Size Purpose
editor.worker.js 1.6 MB Base editor operations
ts.worker.js 22 MB TypeScript/JavaScript IntelliSense
css.worker.js 4.7 MB CSS validation and completion
html.worker.js 3.3 MB HTML validation
json.worker.js 2.2 MB JSON schema validation

Workers are loaded asynchronously and run in separate threads for better performance.

Theme System

Themes are loaded directly as static assets via HTTP fetch (no PHP handler required). Theme preference values include the file extension (e.g., twilight.tmTheme, one-dark-pro.json).

Supported Formats

1. TextMate Themes (.tmTheme) Legacy XML-based themes. Converted to Monaco format at runtime using fast-plist library.

2. JSON Themes (.json) Modern VS Code theme format. Parsed and mapped to Monaco's theme structure.

// codeeditor.js - Themes loaded via static fetch
async fetchTheme(themeName) {
    // Theme name includes extension (e.g., "twilight.tmTheme", "one-dark-pro.json")
    // Legacy values without extension default to .tmTheme
    const basePath = window.Snowboard.url().asset('/modules/backend/formwidgets/codeeditor/assets/themes/');
    const response = await fetch(`${basePath}${themeName}`);
    // Format determined from file extension
}

Usage

Basic Usage

# fields.yaml
code:
    type: codeeditor
    size: giant
    language: php

Available Options

code:
    type: codeeditor
    # Editor size
    size: tiny|small|large|huge|giant  # Default: large

    # Programming language
    language: php|javascript|css|html|twig|yaml|etc  # Default: php

    # Theme (overrides user preference)
    theme: twilight|monokai|github|one-dark-pro|etc

    # Line numbers
    showGutter: true|false  # Default: true

    # Word wrapping
    wordWrap: true|false  # Default: true

    # Code folding
    codeFolding: true|false  # Default: true

    # Auto-closing brackets
    autoClosing: true|false  # Default: true

    # Soft tabs (spaces)
    useSoftTabs: true|false  # Default: true
    tabSize: 2|4|8  # Default: 4

    # Font size (px)
    fontSize: 10|12|14|16|18  # Default: 12

    # Read-only mode
    readOnly: true|false  # Default: false
    disabled: true|false  # Sets readOnly

    # Display options
    showInvisibles: true|false  # Default: false
    highlightActiveLine: true|false  # Default: true
    displayIndentGuides: true|false  # Default: true
    showPrintMargin: true|false  # Default: false
    showMinimap: true|false  # Default: true
    bracketColors: true|false  # Default: false
    showColors: true|false  # Default: true (CSS color picker)

JavaScript API

// Get editor instance
const $editor = $('#my-editor');
const wrapper = $editor.data('oc.codeeditor');

// Access Monaco instance directly
const monacoEditor = wrapper.editor;

// Get/set content (via wrapper)
const code = wrapper.getValue();
wrapper.setValue('function test() {}');

// Get/set language
wrapper.setLanguage('javascript');

// Change theme
wrapper.setTheme('one-dark-pro');

// Insert at cursor
wrapper.insert('code here');

// Get cursor position
const position = wrapper.getPosition();  // { lineNumber: 1, column: 1 }

// Fullscreen
wrapper.enterFullscreen();
wrapper.exitFullscreen();

Migrating from ACE to Monaco API

Winter CMS has migrated from ACE Editor to Monaco Editor. While backward compatibility is maintained for accessing the editor instance via jQuery .data('oc.codeEditor'), direct ACE API calls need to be updated.

Breaking Changes

ACE's Session API is Removed:

  • editor.getSession() → No longer available
  • ACE used a separate "session" object for document operations
  • Monaco combines session and model into a single API

Position Indexing Changed:

  • ACE uses 0-indexed positions (rows and columns start at 0)
  • Monaco uses 1-indexed positions (lines and columns start at 1)
  • Example: ACE row 5 = Monaco line 6, ACE column 0 = Monaco column 1

Annotations Replaced with Markers:

  • ACE's setAnnotations() → Monaco's monaco.editor.setModelMarkers()
  • Different data structure and API

Quick Migration Guide

Getting/Setting Editor Value:

// ❌ OLD (ACE API - Deprecated)
const editor = $('[data-control=codeeditor]').data('oc.codeEditor').editor;
const value = editor.getSession().getValue();
editor.getSession().setValue('new value');

// ✅ NEW (Recommended - Use Wrapper)
const wrapper = $('[data-control=codeeditor]').data('oc.codeEditor');
const value = wrapper.getValue();
wrapper.setValue('new value');

// ✅ ALTERNATIVE (Direct Monaco API)
const monacoEditor = wrapper.editor;
const value = monacoEditor.getModel().getValue();
monacoEditor.getModel().setValue('new value');

Inserting Text at Cursor:

// ❌ OLD (ACE API)
editor.insert('text');

// ✅ NEW (Wrapper provides this method)
wrapper.insert('text');

Working with Annotations/Markers:

// ❌ OLD (ACE Annotations)
editor.getSession().setAnnotations([
    { row: 5, column: 0, text: 'Warning message', type: 'warning' }
]);

// Clear annotations
editor.getSession().setAnnotations([]);

// ✅ NEW (Monaco Wrapper Method - Recommended)
wrapper.setMarkers('sourceId', [
    {
        startLineNumber: 6,        // ACE row 5 = Monaco line 6 (1-indexed!)
        startColumn: 1,            // ACE column 0 = Monaco column 1
        endLineNumber: 6,
        endColumn: Number.MAX_VALUE,  // End of line
        message: 'Warning message',
        severity: wrapper.monaco.MarkerSeverity.Warning  // Info, Warning, or Error
    }
]);

// Clear markers
wrapper.setMarkers('sourceId', []);

Getting Cursor Position:

// ❌ OLD (ACE API)
const cursor = editor.getCursorPosition();  // { row: 5, column: 10 } (0-indexed)

// ✅ NEW (Wrapper)
const position = wrapper.getPosition();  // { lineNumber: 6, column: 11 } (1-indexed)

// ✅ ALTERNATIVE (Direct Monaco)
const position = wrapper.editor.getPosition();

Getting Selection:

// ❌ OLD (ACE API)
const range = editor.getSelection().getRange();

// ✅ NEW (Wrapper)
const selection = wrapper.getSelection();

// ✅ ALTERNATIVE (Direct Monaco)
const selection = wrapper.editor.getSelection();

API Comparison Table

Operation ACE API (Deprecated) Monaco Wrapper (Recommended) Direct Monaco API
Get value editor.getSession().getValue() wrapper.getValue() editor.getModel().getValue()
Set value editor.getSession().setValue(v) wrapper.setValue(v) editor.getModel().setValue(v)
Insert text editor.insert(text) wrapper.insert(text) Complex - use wrapper
Get position editor.getCursorPosition() wrapper.getPosition() editor.getPosition()
Get selection editor.getSelection() wrapper.getSelection() editor.getSelection()
Set annotations editor.getSession().setAnnotations() wrapper.setMarkers(id, markers) monaco.editor.setModelMarkers()
Focus editor editor.focus() wrapper.focus() editor.focus()
Set language N/A wrapper.setLanguage(lang) Complex - use wrapper

Migration Checklist for Plugin Developers

If your plugin interacts with the CodeEditor widget, follow these steps:

  1. Update Editor Instance Access:

    • Keep: .data('oc.codeEditor') - Returns the wrapper
    • ⚠️ Avoid: .data('oc.codeEditor').editor - Returns raw Monaco (advanced use only)
  2. Replace ACE Session Methods:

    • Remove all: getSession().getValue() Use: getValue()
    • Remove all: getSession().setValue() Use: setValue()
  3. Update Annotations:

    • Remove: getSession().setAnnotations(annotations)
    • Add: wrapper.setMarkers(sourceId, markers)
    • ⚠️ Remember: Convert 0-indexed row/column to 1-indexed line/column
    • Use wrapper.monaco.MarkerSeverity for severity constants
  4. Test Thoroughly:

    • Verify all editor interactions work
    • Check that cursor operations use correct indexing
    • Ensure markers/warnings display correctly

Available Wrapper Methods

The Monaco Snowboard editor wrapper provides these convenience methods:

const wrapper = $('[data-control=codeeditor]').data('oc.codeEditor');

// Content
wrapper.getValue()              // Get editor content
wrapper.setValue(value)         // Set editor content
wrapper.insert(text)           // Insert at cursor position

// Position & Selection
wrapper.getPosition()          // Get cursor position (1-indexed)
wrapper.getSelection()         // Get selection range

// Markers (Errors/Warnings/Info with squiggly underlines)
wrapper.setMarkers(sourceId, markers)  // Set error/warning markers in editor
// Example: wrapper.setMarkers('myPlugin', [{ startLineNumber: 5, startColumn: 1,
//   endLineNumber: 5, endColumn: Number.MAX_VALUE, message: 'Warning',
//   severity: wrapper.monaco.MarkerSeverity.Warning }])

// Decorations (Visual highlights WITHOUT error semantics)
wrapper.setDecorations(sourceId, decorations)  // Set visual highlights (no squiggles)
// Example: wrapper.setDecorations('myHighlight', [{ range: new monaco.Range(5, 1, 5, Number.MAX_VALUE),
//   options: { isWholeLine: true, className: 'myHighlightClass',
//   linesDecorationsClassName: 'myGutterClass' } }])

// Configuration
wrapper.setLanguage(lang)      // Change syntax highlighting language
wrapper.setTheme(theme)        // Change color theme
wrapper.focus()                // Focus the editor

// View
wrapper.enterFullscreen()      // Enter fullscreen mode
wrapper.exitFullscreen()       // Exit fullscreen mode
wrapper.refresh()              // Refresh editor (re-create instance)

// Direct Access (Advanced)
wrapper.editor                 // Access Monaco editor instance
wrapper.getEditor()           // Same as wrapper.editor
wrapper.getModel()            // Get Monaco model
wrapper.monaco                // Access Monaco namespace (for constants like MarkerSeverity)

Example: Winter.Builder Plugin Migration

The Winter.Builder plugin was migrated to use Monaco API. Here's a real example:

Before (ACE):

Localization.prototype.copyStringsFromDone = function(data) {
    var codeEditor = this.getCodeEditor($masterTabPane);

    // Set value using ACE Session API
    codeEditor.getSession().setValue(responseData.strings);

    // Set annotations using ACE
    var annotations = [];
    for (var i = 0; i < updatedLines.length; i++) {
        annotations.push({
            row: updatedLines[i],        // 0-indexed
            column: 0,
            text: 'New String',
            type: 'warning'
        });
    }
    codeEditor.getSession().setAnnotations(annotations);
}

After (Monaco):

Localization.prototype.copyStringsFromDone = function(data) {
    var wrapper = this.getCodeEditor($masterTabPane);

    // Set value using wrapper method
    wrapper.setValue(responseData.strings);

    // Convert to Monaco decorations (visual highlights without error semantics)
    var decorations = [];
    for (var i = 0; i < updatedLines.length; i++) {
        decorations.push({
            range: new wrapper.monaco.Range(
                updatedLines[i] + 1,  // Convert to 1-indexed!
                1,                    // Start column
                updatedLines[i] + 1,  // End line (same line)
                Number.MAX_VALUE      // End column (end of line)
            ),
            options: {
                isWholeLine: true,
                className: 'builder-new-translation-line',         // Background highlight
                linesDecorationsClassName: 'builder-new-translation-gutter',  // Gutter indicator
                hoverMessage: { value: 'New string or section' }   // Tooltip on hover
            }
        });
    }
    wrapper.setDecorations('builderLocalization', decorations);
}

Testing

Playwright E2E Tests

Comprehensive test suite with 55+ tests:

# Run all tests
npm run test:e2e

# Run with UI
npm run test:e2e:ui

# Run specific test file
npx playwright test fullscreen.spec.js

# Debug tests
npm run test:e2e:debug

Test Coverage

  • Fullscreen functionality (6 tests)
  • Theme loading and switching (9 tests)
  • Language support (14 tests for all 15 languages)
  • Monaco features (14 tests: find, replace, folding, multi-cursor, etc.)
  • Preferences persistence (12 tests)

See tests/e2e/README-TESTING.md for full testing documentation.

Resources