Skip to Main Content

APEX

Announcement

For appeals, questions and feedback about Oracle Forums, please email oracle-forums-moderators_us@oracle.com. Technical questions should be asked in the appropriate category. Thank you!

Interested in getting your voice heard by members of the Developer Marketing team at Oracle? Check out this post for AppDev or this post for AI focus group information.

Oracle Apex - CKEDITOR - I developed 2 plugins (File uploader, Mathlive)

orkun_tunc_bilgicMar 24 2025 — edited Mar 24 2025

Oracle Apex Version: 24.2
Database: Oracle Cloud 23ai (Apex Workload)


Hello, i want to share my two working plugin for free for everyone. You can add them in your code and use. My plugins are for ckeditor5 which is Oracle's current rich text editor. You can start step by step if you have questions you can comment it.

We have two plugins

  1. Implementing a mathfield by using mathlive plugin.
    1. We can write latex formulas visually by directly using mathfield input with its all functionality.
      1. We can show it in editor by using mathlive markup converter. (We cant use mathml conversion since ckeditor can't show it)
      2. Markup converter generates a long html about 2-3 kb for a short formula. Mathml converter generates 200-300 bytes only. So that's why our code is saving the html (dataDowncast) as mathml but showing it as markup html (editingDowncast).

  2. Implementing a general file uploading.
    1. It supports inline pdf , video , image. Other types would be saved as a link text (depends on user input too)

Js Libraries

First of all we need to have these;

https://cdn.jsdelivr.net/npm/mathlive@0.104.0/dist/mathlive.min.js

https://cdn.jsdelivr.net/npm/@cortex-js/compute-engine@0.28.0/dist/compute-engine.min.js

https://cdn.jsdelivr.net/npm/mathlive@0.104.1/dist/mathlive-static.min.css

Create a rich text editor item.

JS Init Code

function(options){
    return ckeditor_settings(options);
}

Main Code

function ckeditor_settings(options) {    
    // ========================= MathFieldPlugin ===============================

    class MathFieldPlugin {
        constructor(editor) {
            this.editor = editor;
        }

        init() {
            const editor = this.editor;

            editor.model.schema.register('math', {
                inheritAllFrom: '$inlineObject',
                allowAttributes: ['data-mathml', 'data-latex']
            });

            editor.conversion.for('dataDowncast').elementToElement({
                model: 'math',
                view: (modelElement, { writer }) => {
                    const latex = modelElement.getAttribute('data-latex');
                    
                    const mathMl = MathLive.convertLatexToMathMl(latex);
                    const container = writer.createContainerElement('span', {
                        class: 'math-widget',
                        'data-latex': latex,
                        style: 'display: inline-block;max-width: 100%;max-height:100%;white-space: nowrap;overflow-x: auto;overflow-y: hidden;'
                    });
                    writer.insert(
                        writer.createPositionAt(container, 0),
                        writer.createRawElement('math', {}, function(domElement) {
                            domElement.innerHTML = mathMl;
                        })
                    );
                    return container;
                }
            });

            editor.conversion.for('editingDowncast').elementToElement({
                model: 'math',
                view: (modelElement, { writer }) => {
                    const latex = modelElement.getAttribute('data-latex');
                    const mathMl = MathLive.convertLatexToMarkup(latex);
                    const container = writer.createContainerElement('span', {
                        class: 'math-widget ck-widget', 
                        'data-latex': latex,
                        style: 'display:inline-block;',
                        contenteditable: 'false'
                    });
                    writer.insert(
                        writer.createPositionAt(container, 0),
                        writer.createRawElement('span', {}, function(domElement) {
                            domElement.innerHTML = mathMl;
                        })
                    );
                    return ckeditor5.toWidget(container, writer, { label: 'Math formula widget' });
                }
            });

            editor.conversion.for('editingDowncast').add(dispatcher => {
                dispatcher.on('attribute:data-latex:math', (evt, data, conversionApi) => {
                    const viewElement = conversionApi.mapper.toViewElement(data.item);
                    if (viewElement) {
                        const writer = conversionApi.writer;
                        writer.remove(writer.createRangeIn(viewElement));
                        const latex = data.item.getAttribute('data-latex');
                        const mathMl = MathLive.convertLatexToMarkup(latex);
                        writer.insert(
                            writer.createPositionAt(viewElement, 0),
                            writer.createRawElement('span', {}, function(domElement) {
                                domElement.innerHTML = mathMl;
                            })
                        );
                    }
                });
            });

            editor.conversion.for('upcast').elementToElement({
                view: {
                    name: 'span',
                    classes: 'math-widget'
                },
                model: (viewElement, { writer }) => {
                    const latex = viewElement.getAttribute('data-latex');
                    return writer.createElement('math', { 'data-latex': latex });
                }
            });

            editor.ui.componentFactory.add('insertMath', locale => {
                const button = new ckeditor5.ButtonView(locale);
                button.set({
                    label: '', 
                    withText: false,
                    tooltip: 'Insert Formula'
                });

                button.extendTemplate({
                    attributes: {
                        class: 'fa fa-function'
                    }
                });

                button.on('execute', () => {
                    openMathPopup(editor);
                });

                return button;
            });

            editor.editing.view.document.on('click', (evt, data) => {
                if ( data.domEvent.detail === 2 ) {
                    let widgetElement = data.target;
                    if ( !widgetElement.hasClass('math-widget') ) {
                        widgetElement = widgetElement.findAncestor(element => element.hasClass('math-widget'));
                    }
                    if ( widgetElement ) {
                        const modelElement = editor.editing.mapper.toModelElement(widgetElement);
                        openMathPopup(editor, modelElement);
                    }
                }
            });
        }
    }

    const mfe = new MathfieldElement({
        virtualKeyboardToggle: true,
        mathVirtualKeyboardPolicy: 'manual'
    });

    mfe.style.minWidth = '250px';
    mfe.style.display = 'block';
    mfe.id = 'math-input';

    function openMathPopup(editor, modelElement) {
        const textView = new ckeditor5.View(editor.locale);
        textView.setTemplate({
            tag: 'div',
            attributes: {
                style: {
                    'padding': 'var(--ck-spacing-large)',
                    'min-width':'250px',
                    'display':'block',
                    'tabindex': -1,
                    'user-select':'text'
                },
                id:'math-field'
            }
        });

        editor.plugins.get('Dialog').show({
            isModal: false,
            hasCloseButton: false,
            title: 'Insert Formula',
            content: textView,
            actionButtons: [
                {
                    label: 'Insert',
                    class: 'ck-button-action',
                    withText: true,
                    onExecute: () => {
                        const latex = document.getElementById('math-input').value;
                        if (latex) {
                            
                            editor.model.change(writer => {
                                let mathElement;
                                if (modelElement) {
                                    mathElement = modelElement;
                                    writer.setAttribute('data-latex', latex, mathElement);
                                } else {
                                    mathElement = writer.createElement('math', { 'data-latex': latex});
                                    const selection = editor.model.document.selection;
                                    writer.insert(mathElement, selection.getFirstPosition());
                                }
                            });
                        }
                        document.getElementById('math-input').value = "";
                        mathVirtualKeyboard.hide()
                        editor.plugins.get('Dialog').hide();
                    }
                },
                {
                    label: 'Cancel',
                    class: 'ck-button-cancel',
                    withText: true,
                    onExecute: () => {
                        
                        document.getElementById('math-input').value = "";
                        mathVirtualKeyboard.hide()
                        editor.plugins.get('Dialog').hide();
                    }
                }
            ]
        });
        document.querySelector('#math-field').appendChild(mfe);

        if (modelElement) {
            const currentLatex = modelElement.getAttribute('data-latex');
            document.getElementById('math-input').value = currentLatex;
        }
    }

    // ========================= InsertFilePlugin =========================
    class InsertFilePlugin {
        constructor(editor) {
            this.editor = editor;
        }

        init() {
            const editor = this.editor;

            editor.ui.componentFactory.add('insertFile', locale => {
                const button = new ckeditor5.ButtonView(locale);
                button.set({
                    label: '', 
                    withText: false,
                    tooltip: 'Insert File'
                });

                button.extendTemplate({
                    attributes: {
                        class: 'fa fa-upload'
                    }
                });

                button.on('execute', () => {
                    openFilePopup(editor);
                });

                return button;
            });
        }
    }

    function openFilePopup(editor) {
        const fileView = new ckeditor5.View(editor.locale);
        fileView.setTemplate({
            tag: 'div',
            attributes: {
                style: {
                    'padding': 'var(--ck-spacing-large)',
                    'min-width': '300px',
                    'display': 'block'
                },
                id: 'file-upload-field'
            },
            children: [
                {
                    tag: 'input',
                    attributes: {
                        type: 'file',
                        id: 'ckeditor-file-input',
                        style: 'display:block;margin-bottom:10px;'
                    }
                },
                {
                    tag: 'input',
                    attributes: {
                        type: 'text',
                        id: 'file-link-text',
                        placeholder: 'Enter link text (if not image/video/pdf)',
                        style: 'display:block;width:100%;margin-bottom:10px;'
                    }
                }
            ]
        });

        editor.plugins.get('Dialog').show({
            isModal: true,
            hasCloseButton: true,
            title: 'Upload File',
            content: fileView,
            actionButtons: [
                {
                    label: 'Insert',
                    class: 'ck-button-action',
                    withText: true,
                    onExecute: () => {
                        var lSpinner$ = apex.util.showSpinner();
                        $('.ck-button-action').attr('disabled','disabled');
                        $('.ck-button-cancel').attr('disabled','disabled');
                        
                        const fileInput = document.getElementById('ckeditor-file-input');
                        if (fileInput.files.length === 0) {
                            alert(apex.lang.getMessage("CKEDITOR_SELECT_FILE_VALIDATION"));
                            return;
                        }
                        const file = fileInput.files[0];

                        const isImage = file.type.startsWith('image/');
                        const isVideo = file.type.startsWith('video/');
                        const isPDF = file.type.startsWith('application/pdf');

                        if (file) {
                            var reader = new FileReader();
                            reader.onload = function(e) {
                                var base64Data = e.target.result.split(',')[1];
                               
                                apex.server.process("AP_UPLOAD_FILE", { 
                                    p_clob_01: base64Data,             
                                    x01: file.name,             
                                    x02: file.type,               
                                    x03: isImage || isVideo || isPDF ? 'inline':'attachment'
                                }, {
                                    success: function(pData) {
                                        const fileUrl = pData.url;
                                        editor.model.change(writer => {
                                            if(isImage){
                                                editor.execute( 'insertImage', { source: fileUrl } );
                                            }
                                            
                                            else if (isVideo || isPDF) {
                                                editor.execute( 'mediaEmbed', fileUrl );
                                            } else {
                                                
                                                let linkText = document.getElementById('file-link-text').value;
                                                if (!linkText) {
                                                    linkText = 'Download File';
                                                }
                                                const insertPosition = editor.model.document.selection.getFirstPosition();
                                                const textElement = writer.createText(linkText, { linkHref: fileUrl });
                                                writer.insert(textElement, insertPosition);

                                            }
                                        });
                                        lSpinner$.remove();
                                        editor.plugins.get('Dialog').hide();
                                    },
                                    error: function(jqXHR, textStatus, errorThrown) {
                                        lSpinner$.remove();
                                        editor.plugins.get('Dialog').hide();
                                    }
                                });
                            };
                            reader.readAsDataURL(file);
                        } else {
                            alert(apex.lang.getMessage("CKEDITOR_SELECT_FILE_VALIDATION"));
                        }
                    }
                },
                {
                    label: 'Cancel',
                    withText: true,
                    class: 'ck-button-cancel',
                    onExecute: () => {
                        editor.plugins.get('Dialog').hide();
                    }
                }
            ]
        });
    }

    // ========================= CKEditor Settings =============================

    options.editorOptions.mediaEmbed = {
        previewsInData: true, 
        providers: [
            // YouTube
            {
                name: 'youtube',
                url: /^https?:\/\/(www\.)?youtube\.com\/watch\?v=([\w-]+)/,
                html: match => {
                    const videoId = match[2]; 
                    return `<iframe width="100%" height="300px" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
                }
            },
            {
                name: 'youtube-short',
                url: /^https?:\/\/youtu\.be\/([\w-]+)/,
                html: match => {
                    const videoId = match[1];
                    return `<iframe width="100%" height="300px" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
                }
            },

            // Vimeo
            {
                name: 'vimeo',
                url: /^https?:\/\/(www\.)?vimeo\.com\/(\d+)/,
                html: match => {
                    const videoId = match[2]; 
                    return `<iframe width="100%" height="300px" src="https://player.vimeo.com/video/${videoId}" frameborder="0" allowfullscreen></iframe>`;
                }
            },

            // Dailymotion
            {
                name: 'dailymotion',
                url: /^https?:\/\/(www\.)?dailymotion\.com\/video\/([\w-]+)/,
                html: match => {
                    const videoId = match[2]; 
                    return `<iframe width="100%" height="300px" src="https://www.dailymotion.com/embed/video/${videoId}" frameborder="0" allowfullscreen></iframe>`;
                }
            } // You can add more
        ]
    };
    
    options.editorOptions.extraPlugins = [
        ckeditor5.MediaEmbed,
        ckeditor5.ImageResizeEditing,
        ckeditor5.ImageResizeHandles,
        ckeditor5.Widget,
        ckeditor5.Base64UploadAdapter,
        ckeditor5.MediaEmbedEditing,
        MathFieldPlugin,
        InsertFilePlugin
    ];
    
    options.editorOptions.toolbar = [
        "heading",
        "|",
        "undo",
        "redo",
        "|",
        "bold",
        "italic",
        "underline",
        "strikethrough",
        "subScript",
        "superScript",
        "code",
        "removeFormat",
        "|",
        "fontSize",
        "fontFamily",
        "fontColor",
        "fontBackgroundColor",
        "highlight",
        "|",
        "alignment",
        "indent",
        "outdent",
        "|",
        "bulletedList",
        "numberedList",
        "todoList",
        "|",
        "specialCharacters",
        "insertMath",
        "insertFile" ,
        "insertImageViaUrl",
        "mediaEmbed",
        "link",
        "blockQuote",
        "pageBreak",
        "horizontalLine",
        "insertTable",
        "codeBlock"
    ];

    return options;
}

Application Process for uploading file & returning URL

I'm using global page for P0_FILE_ID & P0_DISPOSITION and both has checksum - application based in order to keep the links working and secure within application.

declare
    l_base64      clob := apex_application.g_clob_01;
    l_file_name   app_ses.bucket_file.file_name%Type   := apex_application.g_x01; 
    l_mime_type   app_ses.bucket_file.mime_type%Type   := apex_application.g_x02;
    l_disposition Varchar2(10 Char)   := apex_application.g_x03;
    l_blob        blob;
    v_file_id     number;
    v_url         Varchar2(500 Char);
begin
    l_blob := APEX_WEB_SERVICE.CLOBBASE642BLOB (p_clob => l_base64);
    
    -- Upload file
    upload(
        p_blob_file     => l_blob,
        p_content_type  => l_mime_type,
        p_file_name     => l_file_name,
        p_file_id       => v_file_id
    );

    v_url := APEX_PAGE.GET_URL (
            p_page               => 0,
            p_application        => 123,
            p_request            => 'APPLICATION_PROCESS=AP_GET_OBJECT',
            p_items              => 'P0_FILE_ID,P0_DISPOSITION',
            p_values             => v_file_id||','||l_disposition,
            p_plain_url          => true,
            p_absolute_url       => true );

    apex_json.open_object();
        apex_json.write('status', 'success');
        apex_json.write('url', v_url);
    apex_json.close_object();  
end;

Application Process 2 - AP_GET_OBJECT

Declare
    v_file_name bucket_file.file_name%type;
    v_object_name bucket_file.bucket_path%type;
    v_mime_type bucket_file.mime_type%type;
    v_disposition Varchar2(10 Char);
begin
    Select file_name, bucket_path, mime_type
      Into v_file_name, v_object_name, v_mime_type
      From bucket_file
     Where id = :P0_FILE_ID;

    Case :P0_DISPOSITION
        When 'auto' Then
            Case 
                When v_mime_type like 'image/%' or v_mime_type like 'video/%' or v_mime_type = 'application/pdf' Then
                    v_disposition := 'inline';
                Else
                    v_disposition := 'attachment';
            End Case;     
        Else
            v_disposition := :P0_DISPOSITION;
    End Case;

    pkg_bucket.p_get_file( 
        p_object_name => v_object_name,
        p_file_name => v_file_name,
        p_disposition => :P0_DISPOSITION);
end;

P_GET_FILE Procedure →

I'm using oracle object storage for file storing. But the point is you need to have mimetype, filename & blob content. So you can adjust your code same by changing storage with your blob column or object storage or aws doesnt matter.

    Procedure p_get_file (
        p_object_name Varchar2,
        p_file_name Varchar2,
        p_disposition Varchar2 Default 'attachment'
    ) Is
        l_file  dbms_cloud_oci_obs_object_storage_get_object_response_t ;
        v_region Varchar2(500 Char);
        v_bucket_namespace Varchar2(500 Char);
        v_bucket_name   Varchar2(500 Char);
    Begin

        Select value into v_bucket_name from parameters where name = 'BUCKET_NAME';
        Select value into v_bucket_namespace from parameters where name = 'BUCKET_NAMESPACE';
        Select value into v_region from parameters where name = 'CLOUD_REGION_NAME';
        
        l_file := dbms_cloud_oci_obs_object_storage.get_object(
                    namespace_name  => v_bucket_namespace,
                    bucket_name     => v_bucket_name,
                    object_name     => p_object_name,
                    region          => v_region,
                    credential_name => gv_credential_name );

        owa_util.mime_header(l_file.headers.get_String('Content-Type'), false);
        htp.p('Cache-Control: public, max-age=86400, immutable'); 
        htp.p('Content-Disposition: '|| p_disposition || '; filename="'||p_file_name||'"');
        OWA_UTIL.HTTP_HEADER_CLOSE; 

        WPG_DOCLOAD.DOWNLOAD_FILE(p_blob => l_file.response_body);
    End p_get_file;

Thank you for reading my post ^_^ .

Comments

Post Details

Added on Mar 24 2025
0 comments
335 views