5

I have my CKEDITOR form prepopulated with hidden table which is being submitted together with user inputed text. This works fine, but sometimes user presses backspace too many times and deletes the hidden table.

Is there a way to block editing on this hidden table inside ckeditor textarea? So when user presses backspace the hidden table isn't affected and stays in.

As soon as CKEDITOR instance is ready this source (bellow) is put inside CkEditor Textarea (using setData() attribute) and User only sees the returned <p></p> value. In this case its <p>I really think I can do this!</p>. Its a description of his profile and he can keep it and edit it. The rest is hidden and only visible in e-mail when form is submitted. Its strange that <p></p> is on top but if user presses Backspace couple times the table gets deleted and therefor not submitted.

    <span id="messageTemplate1" class="message">

<p>I really think I can do this!</p>

<table class="hide" style="font-size: 12px;">
    <tbody>
        <tr class="hide">
            <td>
            Application sent by <strong><a href="http://www.globalcastingcenter.com/talent/jack-bolton">Matt Faro</a></strong> for Audition: <a href="http://www.globalcastingcenter.com:80/CustomContentRetrieve.aspx?ID=4185493">Actors Needed</a>
            </td>
        </tr>
        <tr class="hide">
            <td>
            Reply to applicant directly: mantas@mantas.co or visit full profile: http://www.globalcastingcenter.com/talent/jack-bolton
            </td>
        </tr>
    </tbody>
</table>
<table class="hide" style="font-size: 12px;">
    <tbody>
        <tr class="hide">
            <td><strong>Short Profile Summary:</strong></td>
        </tr>
    </tbody>
</table>
<table class="hide" style="font-size: 12px;">
    <tbody>
        <tr class="hide">
            <td>
            <a href="http://www.globalcastingcenter.com/talent/jack-bolton"><img alt="" src="http://globalcastingcenter.com/talent_images/4164035_258551_foto.png?Action=thumbnail&amp;Width=144&amp;Height=215" /></a>
            </td>
        </tr>
    </tbody>
</table>
<table style="font-size: 12px;" class="hide">
    <tbody>
        <tr class="hide">
            <td><strong>Areas:</strong></td>
            <td>Actor,Extra</td>
        </tr>
        <tr class="hide">
            <td><strong>Country:</strong></td>
            <td>WORLDWIDE,Any</td>
        </tr>
        <tr class="hide">
            <td><strong>Age:</strong></td>
            <td>26</td>
        </tr>
    </tbody>
</table>
</span>

Now when I load your plugin my CKeditor box disapears, please press "Apply" on testing page http://gcc-july.themantas.co.uk/auditions/actors-needed please login first to be able to access the message box Login name: tiknius@gmail.com pssw: test

My config file:

CKEDITOR.editorConfig = function( config )
{
    config.toolbar = 'MyToolbar';

    config.toolbar_MyToolbar =
    [

        { name: 'clipboard', items : [ 'Undo','Redo' ] },           
        { name: 'styles', items : ['FontSize' ] },
        { name: 'basicstyles', items : [ 'Bold','Italic'] },
        { name: 'paragraph', items : ['Outdent','Indent' ] },

    ];

    config.removePlugins = 'contextmenu';
    config.forcePasteAsPlainText = true;   
    config.pasteFromWordRemoveFontStyles = true;
    config.pasteFromWordRemoveStyles = true;
    config.extraPlugins = 'cwjdsjcsconfineselection';
    config.startupShowBorders = false;
    config.disableObjectResizing = true;

};

This is how the box looks when I disable your plugin: http://screencast.com/t/Kc2bIOU8md2

I use your suggested HTML structure.

codewaggle
  • 4,893
  • 2
  • 32
  • 48
Mantas
  • 169
  • 1
  • 4
  • 12
  • I had a situation where users started with a table, we wanted to limit the user to editing within the table and prevent them from changing one cell in that table. I created a plugin to accomplish this. Can you give more details about what the user starts with? Is the hidden table inside any other containers? Is there a main container that the user works within? Showing the HTML contained within the editor would be helpful, you could click the source button, then copy and paste that into your question. The content isn't important, just the structure with classes and ID's of the elements. – codewaggle Jul 05 '12 at 03:40
  • Thanks for you comment codewaggle. I've added extra explanation and the source, please let me know what you think. – Mantas Jul 06 '12 at 10:17
  • My plugin will handle this, I'll clean it up and put it in an answer within a few hours. – codewaggle Jul 06 '12 at 12:36

3 Answers3

4

I had to play around with it a bit to get it to work. I added lots of documentation to the plugin code, if you have any questions after reading it through let me know.

I'm including an updated version of your content block and the plugin code block.


Here is your updated content block. It wasn't working when wrapped in the <span> tag, so I wrapped it in a table.

You may not like the border and resizing outlines that appear around the data cell, if that's the case, add these settings to your configuration:
config.startupShowBorders = false;
config.disableObjectResizing = true;

Some notes:
The empty <td> before your starting content is needed, it prevents the user from using "Ctrl A" to select everything which would allow them to delete the hidden table.

I removed the <p> tag from the starting content as it acted funky in this structure.

The <td> that holds the hidden tables has a &nbsp; character, it prevents the user from using "Ctrl A" to select everything which would allow them to delete the hidden table. It causes the cursor to get lost if you delete everything to the right of the cursor, but you can click on the content to begin editing again.

The contenteditable="false" attribute is used by CkEditor and is needed, but it doesn't do the whole job. You can try out the new HTML without activating the plugin to see what effect it has by itself.

There are notes in the plugin code about the classes and ID I used.

<!-- Begin Wrapper Table that Replaces <span> element -->
<table id="messageTemplate1" class="message cwjdsjcs_editable">
    <tbody>
        <tr>
            <td class="cwjdsjcs_not_editable" contenteditable="false">
            </td>
            <td id="cwjdsjcs_editable_id">
                I really think I can do this!
            </td>
        </tr>

        <tr class="cwjdsjcs_not_editable" contenteditable="false">
            <td colspan="2">
                &nbsp;

                <!-- Begin Original Content -->
                <table class="hide" style="font-size: 12px; display:none;">
                    <tbody>
                        <tr class="hide">
                            <td>
                            Application sent by <strong><a href="http://www.globalcastingcenter.com/talent/jack-bolton">Matt Faro</a></strong> for Audition: <a href="http://www.globalcastingcenter.com:80/CustomContentRetrieve.aspx?ID=4185493">Actors Needed</a>
                            </td>
                        </tr>
                        <tr class="hide">
                            <td>
                            Reply to applicant directly: mantas@mantas.co or visit full profile: http://www.globalcastingcenter.com/talent/jack-bolton
                            </td>
                        </tr>
                    </tbody>
                </table>
                <table class="hide" style="font-size: 12px; display:none;">
                    <tbody>
                        <tr class="hide">
                            <td><strong>Short Profile Summary:</strong></td>
                        </tr>
                    </tbody>
                </table>
                <table class="hide" style="font-size: 12px; display:none;">
                    <tbody>
                        <tr class="hide">
                            <td>
                            <a href="http://www.globalcastingcenter.com/talent/jack-bolton"><img alt="" src="http://globalcastingcenter.com/talent_images/4164035_258551_foto.png?Action=thumbnail&amp;Width=144&amp;Height=215" /></a>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <table style="font-size: 12px; display:none;" class="hide">
                    <tbody>
                        <tr class="hide">
                            <td><strong>Areas:</strong></td>
                            <td>Actor,Extra</td>
                        </tr>
                        <tr class="hide">
                            <td><strong>Country:</strong></td>
                            <td>WORLDWIDE,Any</td>
                        </tr>
                        <tr class="hide">
                            <td><strong>Age:</strong></td>
                            <td>26</td>
                        </tr>
                    </tbody>
                </table>
                <!-- End Original Content -->

            </td>
        </tr>
    </tbody>
</table>
<!-- End Wrapper Table that Replaces <span> element -->

Here's the plugin code, it's called "cwjdsjcsconfineselection".

To add the plugin:
Create a folder called "cwjdsjcsconfineselection" in the plugins directory: ckeditor/plugins/
Create a file called "plugins.js" in that directory and paste the code below into that file. My mistake: file is named plugin.js, not plugin(s).js.

If you already have extra plugins, add "cwjdsjcsconfineselection" to the extraPlugins config setting, otherwise add this setting to your configuration:
config.extraPlugins = 'cwjdsjcsconfineselection';

The plugin should work the next time you load the editor.

For my situation, I have a dialog box appear when the user clicks in a non-editable area to explain why the cursor was moved back to the previous selection. That doesn't seem necessary for your usage, so I commented it out.

/*
  Plugin that prevents editing of elements with the "non-editable" class as well as elements outside of blocks with "editable" class.
*/

//* **************************  NOTES  ***************************  NOTES  ****************************
/*
  The "lastSelectedElement" variable is used to store the last element selected.

  This plugin uses the "elementspath" plugin which shows all elements in the DOM
  parent tree relative to the current selection in the editing area.

  When the selection changes, "elementsPathUpdate" is fired,
  we key on this and loop through the elements in the tree checking the classes assigned to each element.

  Three outcomes are possible.

  1) The non-editable class is found:
  Looping stops, the current action is cancelled and the cursor is moved to the previous selection.
  The "selectionChange" hook is fired to set the reverted selection throughout the instance.

  2) The editable class is found during looping, the "in_editable_area" flag is set to true.

  3) Neither the editable or the non-editable classes are found (user clicked outside your main container).
  The "in_editable_area" flag remains set to false.

  If the "in_editable_area" flag is false, the current action is cancelled and the cursor is moved to the previous location.
  The "selectionChange" hook is fired to set the reverted selection throughout the instance.

  If the "in_editable_area" flag is true,
  the "lastSelectedElement" is updated to the currently selected element and the plugin returns true.

---------------
  If you don't want the elements path to be displayed at the bottom of the editor window,
  you can hide it with CSS rather than disabling the "elementspath" plugin.

  The elementspath plugin creates and is left active because we are keying on changes to the path in our plugin.
  #cke_path_content
  {
    visibility: hidden !important;
  }

---------------
  CSS Classes and ID that the plugin keys on. Use defaults or update variables to use your preferred classes and ID:

  var starting_element_id = ID of known editable element that always occurs in the instance.
  Don't use elements like <table>, <tr>, <br /> that don't contain HTML text.
  Default value = cwjdsjcs_editable_id

  var editable_class = class of editable containers.
  Should be applied to all top level elements that contain editable elements.
  Default = cwjdsjcs_editable

  var non_editable_class = class of non-editable elements within editable containers
  Apply to elements where all child elements are non-editable.
  Default = cwjdsjcs_not_editable

*/

//* **************************  END NOTES  ***************************  END NOTES  ****************************


// Register the plugin with the editor.
// http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.plugins.html
CKEDITOR.plugins.add( 'cwjdsjcsconfineselection',
{
  requires : [ 'elementspath' ],

  // The plugin initialization logic goes inside this method.
  // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.pluginDefinition.html#init
  init: function( editor )
  {
    editor.on( 'instanceReady', function( instance_ready_data )
    {
      // Create variable that will hold the last allowed selection (for use when a non-editable selection is made)
      var lastSelectedElement;
      editor.cwjdsjcs_just_updated = false;

      // This section starts things off right by selecting a known editable element.
      // *** Enter the ID of the element that should have initial focus *** IMPORTANT *** IMPORTANT ***
      var starting_element_id = "cwjdsjcs_editable_id";

      var resInitialRange = new CKEDITOR.dom.range( editor.document );

      resInitialRange.selectNodeContents( editor.document.getById( starting_element_id ) );
      resInitialRange.collapse();

      var selectionObject = new CKEDITOR.dom.selection( editor.document );

      editor.document.focus();
      selectionObject.selectRanges( [ resInitialRange ] );

      var sel = editor.getSelection();
      var firstElement = sel.getStartElement();
      var currentPath = new CKEDITOR.dom.elementPath( firstElement );

      // Set path for known editable element, fire "selectionChange" hook to update selection throughout instance.
      editor._.selectionPreviousPath = currentPath;
      editor.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } );
    }); // *** END - editor.on( 'instanceReady', function( e )


    // When a new element is selected by the user, check if it's ok for them to edit it,
    // if not move cursor back to last know editable selection
    editor.on( 'elementsPathUpdate', function( resPath )
    {
      // When we fire the "selectionChange" hook at the end of this code block, the "elementsPathUpdate" hook fires.
      // No need to check because we just updated the selection, so bypass processing.
      if( editor.cwjdsjcs_just_updated == true )
      {
        editor.cwjdsjcs_just_updated = false;
        return true;
      }

      var elementsList = editor._.elementsPath.list;
      var in_editable_area = false;
      var non_editable_class = "cwjdsjcs_not_editable";
      var editable_class = "cwjdsjcs_editable";

      for(var w=0;w<elementsList.length;w++){
        var currentElement = elementsList[w];

        // Sometimes a non content element is selected, catch them and return selection to editable area.
        if(w == 0)
        {
          // Could change to switch.
          if( currentElement.getName() == "tbody" )
          {
            in_editable_area = false;
            break;
          }

          if( currentElement.getName() == "tr" )
          {
            in_editable_area = false;
            break;
          }
        }

        // If selection is inside a non-editable element, break from loop and reset selection.
        if( currentElement.hasClass(non_editable_class) )
        {
          in_editable_area = false;
          break;
        }

        if( currentElement.hasClass(editable_class) ) {
          in_editable_area = true;
        }
        console.log(currentElement);
        console.log(currentElement.getName());
      }

      // if selection is within an editable element, exit the plugin, otherwise reset selection.
      if( in_editable_area ) {
        lastSelectedElement = elementsList[0];
        return true;
      }

      var resRange = new CKEDITOR.dom.range( editor.document );

      resRange.selectNodeContents( lastSelectedElement );
      resRange.collapse();
      editor.getSelection().selectRanges( [ resRange ] );
      resRange.endContainer.$.scrollIntoView();

      // Open dialog window:
      // It tells user they selected a non-editable area and cursor has been returned to previous selection
//      currentEditorName = editor.name;
//      openResDefaultDialog(currentEditorName);

      try
      {
        var sel = editor.getSelection();
        var firstElement = sel.getStartElement();
        var currentPath = new CKEDITOR.dom.elementPath( firstElement );
        editor.cwjdsjcs_just_updated = true;

        editor._.selectionPreviousPath = currentPath;
        editor.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } );

      }
      catch (e)
      {}
    });
  } // *** END - init: function( editor )
}); // ************************************************************************************* END - CKEDITOR.plugins.add

To test that the plugin is loaded add an alert after the instance ready trigger:

    editor.on( 'instanceReady', function( instance_ready_data )
    {
      alert("instanceReady");

To test that the plugin is being triggered when the selection changes, add an alert after the elementsPathUpdate trigger:

    editor.on( 'elementsPathUpdate', function( resPath )
    {
      alert("elementsPathUpdate");
codewaggle
  • 4,893
  • 2
  • 32
  • 48
  • Thanks. Please find added info to my question. The ckeditor box disapears when I enabble your plugin. What could cause this? – Mantas Jul 11 '12 at 11:37
  • I looked through the source code of your page with firebug and didn't see the command to replace the textarea with CkEditor: `CKEDITOR.replace( 'xxx' );`. If that's not the problem, try putting the `alert()` after the `instanceReady` as shown at then end of my answer, then comment out the rest of the code. That will show you if the plugin is loading. If the alert works, uncomment sections until the problem occurs again. It's just after 11 AM by me, if you're available, we can talk in the chat area while you work on it. – codewaggle Jul 11 '12 at 15:16
  • Area loads in line 245 where class is set to "ckeditor". When its replaced Template1 is put in it using setData(). It all works fine unless I put the name of the plugin in config Extra plugins line. I dont get alert messages as well when inserted. The same happens when I put any random name in config file Extra plugins line. I can only assume the plugin doesn't load for some syntax or directory reason. I have created a folder "cwjdsjcsconfineselection" in ckeditor plugins directory and plugins.js inside the folder. I'm referencing cwjdsjcsconfineselection in config file. – Mantas Jul 12 '12 at 16:49
  • also theres no such plugin folder as 'elementspath' in my directory but it works as default and is not disabled. – Mantas Jul 12 '12 at 17:01
  • @Mantas I created a chat room called CkEditor – codewaggle Jul 12 '12 at 17:07
  • Thanks, it seems I cant participate in the chat because I only have 5 reputation points and it requires 20 :/ would you consider google talk or skype maybe? My google is tiknius@gmail.com and skype: nerealiakas – Mantas Jul 12 '12 at 17:20
  • @Mantas Do you use `CKEDITOR.replace( 'xxx' );` anywhere? Try chat again and paste the code block with `replace()` in it. – codewaggle Jul 12 '12 at 17:42
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/13806/discussion-between-codewaggle-and-mantas) – codewaggle Jul 12 '12 at 17:47
2

I realize that this is closed and solved, but here's an option:

Add the table just in time after the user submits your form or however your CKE content is being used. Simply don't add the invisible table, but when the user clicks "submit", add it then to whatever is being posted. Alternatively, if you need to edit it later, just remove it before inserting to the editor and then add it once again Just In Time before posting. No hacking of the CKE core and no plugins required as the action happens outside CKE.

Joel Peltonen
  • 13,025
  • 6
  • 64
  • 100
  • Amazingly simple, great idea. When I did this for my own project, it was for a content block that needed to be visible within the editor, so I was locked in on that, but for this situation your suggestion is a much better approach. – codewaggle Sep 26 '12 at 23:44
0

I had a similar requirement but didn't find my answer online.

In my case, I want to support the tab character in the editor by adding a pre element to the content:

editor.on('key', function(ev) {
    if (ev.data.keyCode == 9) { // TAB
        // data-tab attribute added so we can identify it later
        var tabHtml = '<pre data-tab="true" style="display:inline;">&#09;</pre>';
        var tabElement = CKEDITOR.dom.element.createFromHtml(tabHtml, editor.document);
        editor.insertElement(tabElement);
        ev.cancel();
    }
});

But now the user can use the arrow keys or click the mouse and start editing inside the pre element. So I listen to the 'click' and 'keyup' events and move the cursor if they are inside the pre.

// this prevents the user from typing inside the tab span
var moveCursorOutOfTab = function() {
    var ranges = editor.getSelection().getRanges();
    if (ranges.length == 1) {
        var parent = ranges[0].startContainer.getParent();

        // is the cursor is inside a tab element
        if (parent && parent.getAttribute('data-tab') == 'true') {
            var newRange = editor.createRange();
            newRange.setStartAfter(parent);
            newRange.setEndAfter(parent);

            // move the cursor after the tab element
            editor.getSelection().selectRanges([newRange]);
        }
    }
};

// wait until the editor is initialized
editor.on('contentDom', function() {
    var editable = editor.editable();

    // listen for clicks
    editable.attachListener(editable, 'click', function(ev) {
        moveCursorOutOfTab();
    });

    // must use the keyup event because we want to
    // act after any cursor movements
    editor.document.on('keyup', function(ev) {
        // check for left and right arrow keys
        if (ev.data.getKey() == 37 || ev.data.getKey() == 39) {
            moveCursorOutOfTab();
        }
    });
});
lance-java
  • 25,497
  • 4
  • 59
  • 101