Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Block/inline 'meta markup' in CKEditor

I'm working on a custom CMS where CKEditor (4.5.1) is integrated for friendly HTML content generation.

One of the features we're shipping is the ability to restrict parts of the page to specific groups of users and the cleanest way to do that as far as I could tell was create a new tag and use that for tracking of content, e.g. <restrict data-usertype="1,2,3">restricted content for user types 1, 2, 3 here</restrict> which would be stripped out by the backend.

The problem I have is that my custom tag implicitly needs to support both block and inline tags, and I'm not sure how to set this up correctly.

I've tried a variety of combinations of things which either disallow any content being added at all, or disable the plugin entirely (because it falls foul of ACF's own sanity checking); right now the configuration I have will let me add the <restrict> block, will let me edit it in the dialog (including by double-clicking) but will not let me nest any content of any kind and will cause CKEditor to throw a 'could not read attributes of null' warning when switching back to source mode.

My current configuration of this plugin is as follows:

    CKEDITOR.dtd.restrict = {
        a: 1, abbr: 1, address: 1, area: 1, article: 1, aside: 1, audio: 1, b: 1, bdi: 1, bdo: 1, blockquote: 1,
        br: 1, button: 1, canvas: 1, cite: 1, code: 1, command: 1, datalist: 1, del: 1, details: 1, dfn: 1, div: 1,
        dl: 1, em: 1, embed: 1, fieldset: 1, figure: 1, footer: 1, form: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1,
        header: 1, hgroup: 1, hr: 1, i: 1, iframe: 1, img: 1, input: 1, ins: 1, kbd: 1, keygen: 1, label: 1, map: 1,
        mark: 1, meter: 1, noscript: 1, object: 1, ol: 1, output: 1, progress: 1, p: 1, pre: 1, q: 1, ruby: 1, s: 1,
        samp: 1, script: 1, section: 1, select: 1, small: 1, span: 1, strong: 1, sub: 1, sup: 1, table: 1,
        textarea: 1, time: 1, u: 1, ul: 1, 'var': 1, video: 1, wbr: 1, '#': 1
    }; // Allow <restrict> as a valid tag.
    CKEDITOR.dtd.$block.restrict = 1;
    CKEDITOR.dtd.$inline.restrict = 1;
    CKEDITOR.dtd.$blockLimit.restrict = 1; // Treat <restrict> as a block limiter tag
    CKEDITOR.dtd.$removeEmpty.restrict = 1; // Remove <restrict /> tags if they are empty
    CKEDITOR.dtd.$transparent.restrict = 1; // Treat the tag as transparent as far as content models go
    CKEDITOR.dtd.body.restrict = 1; // Allow it in the body, div and p tags.
    CKEDITOR.dtd.div.restrict = 1;
    CKEDITOR.dtd.p.restrict = 1;

    var allowedEls = ['restrict'];
    for (var i in CKEDITOR.dtd.restrict) {
        if (CKEDITOR.dtd.restrict.hasOwnProperty(i) && i != '#') {
            allowedEls.push(i);
        }
    }

    // Define the widget.
    editor.widgets.add( 'restrict', {
        button: 'Restricted Content',
        dialog: 'restrictDialog',
        template: '<restrict />',
        editables: {},
        allowedContent: allowedEls.join(' ') + '[*]{*}(*)', // All the above elements, with any attributes, styles or classes.
        requiredContent: 'restrict[data-*]',
        upcast: function (element) {
            return element.name == 'restrict';
        },
        init: function () {
            // Some stuff which iterates through the various
            // properties I care about, grabs from data
            // attributes and pushes to this.setData().
        },
        data: function () {
            // Some stuff that just fetches vars from this.data,
            // sets the relevant data attribute and also sets an
            // attribute on the span created by CKEditor since
            // styling and ::before content is used to show who
            // the block is visible to - the result is much like
            // the Show Blocks plugin. This stuff all works
            // correctly and being omitted changes nothing.
        }
    } );

I guess I've set up editables incorrectly and probably the general allowed content stuff for this tag, but I can't see how I'm supposed to create such a tag, and I can't imagine that creating such a phantom tag that will be parsed outside of the browser would be a new problem.

Thanks in advance!

like image 468
Jason Spicer Avatar asked Aug 07 '15 13:08

Jason Spicer


1 Answers

I actually found an alternative answer to this problem, mostly by redefining the problem a little.

Firstly, I realised that almost all the time the CMS will be restricting content, it will be doing it in a block context rather than an inline one anyway - entire div sections would be removed rather than parts of a line.

Once I realised that, I then realised it's not really a widget that I needed to build but a more conventional plugin.

I then started by taking the insert-div plugin for CKEditor, and beginning to customise it to suit. In the plugin's init method, there is the following initial setup:

CKEDITOR.dtd.restrict = {};
CKEDITOR.tools.extend(CKEDITOR.dtd.restrict, CKEDITOR.dtd.div);
CKEDITOR.dtd.$block.restrict = 1;
CKEDITOR.dtd.$blockLimit.restrict = 1; // Treat <restrict> as a block limiter tag
CKEDITOR.dtd.$removeEmpty.restrict = 1; // Remove <restrict /> tags if they are empty
CKEDITOR.dtd.body.restrict = 1; // Allow it in the body, div and p tags.
CKEDITOR.dtd.div.restrict = 1;

This largely set up the plugin's needs but I'm working from a pre-built copy of CKEditor, and I have neither the time nor inclination to set up a process to rebuild/reminify CKEditor - which is a problem here because $blockLimit is used at start-up only and adding to it at plugin level doesn't work because the only time it's evaluated has already been run before plugins are loaded. To counter this, the plugin clones the closure and definition of CKEDITOR.dom.editorPath after the plugin has run so it re-evaluates the DTD methods correctly.

Other than that, I changed all the 'creatediv'/'editdiv'/'removediv' references to '*restrict' appropriately, changed the places it checks for 'div' to 'restrict' and replaced the dialog definition with the dialog I actually need, which is very use-case specific. Since it is mostly a copy/paste job, it doesn't really seem useful to provide what code I can, as the bits that aren't copy/paste are specific to the product.

What made this more complex is the fact that I also have styling dynamically managed on the restrict element, to produce a look similar to that of the Show Blocks tag, where the list of restrictions is documented in the tag itself through restrict::before { content: '...' } which points to an attribute on the restrict tag itself, cke-restrict-description, which is updated every time the dialog is run as well as every time the editor switches to WYSIWYG mode (once on instanceReady, once on editor.on('mode') where editor.mode == 'wysiwyg')

like image 76
Jason Spicer Avatar answered Oct 21 '22 14:10

Jason Spicer