Widgetizing WYSIWYG

http://2015.drupalcorn.org/sessions/widgetizing-wysiwyg

Slides: http://bit.ly/drupalcorn-widgets

Code: https://github.com/brandonneil/campwidgets

DrupalCorn Camp 2015

#drupalcorn

Brandon Neil

@brandonneil

bneil

The University of Iowa

Widgetizing WYSIWYG

with CKEditor 4

Assumptions

What we're covering

  • What's a widget?
  • Widget examples
  • Utilizing a 'contrib' widget in Drupal
  • Creating our own widget

Drupal == Structure

  • Content types
  • Fields
  • View Modes
  • Image Styles

¯\_(ツ)_/¯

Structure?

Empty WYSIWYG text area

CKEditor 4

Rich text editor

What You See Is What You Get

  • Open source
  • Accessible
  • Advanced Content Filter (ACF) - 4.1+
  • Widgets - 4.3+

CKEditor 4

Rich text editor

What You See Is What You Get

  • Open source
  • Accessible
  • Advanced Content Filter (ACF) - 4.1+
  • Widgets - 4.3+

Widgets

Widgets are special rich content units in that they are groups of elements which are treated as a single entity inside the editor.
- CKEditor documentation

Characteristics of a Widget

  • Structure is immutable
  • Instance can be selected as a whole and moved around freely
  • Individual parts can be edited
  • Easy to enforce consistent styling of entire widget
  • Can limit available elements inside of a widget

Examples

Drupal Modules that utilize CKEditor Widgets

Utilizing a 'contrib' widget in Drupal

Task: build a Drupal module that adds a plugin (Layout Manager) to CKEditor.

CKEditor widgets are plugins

Plugins can be found in the CKEditor plugin repository

http://ckeditor.com/addons/plugins/all

(or on github, etc)

Time to download things

  • CKEditor 4 Library
  • CKEditor 4 Widget Plugin
  • CKEditor or WYSIWYG module

CKEditor Library Download

http://docs.ckeditor.com/#!/guide/dev_widget_installation

Use CKEditor Builder

http://ckeditor.com/builder

WYSIWYG Module

https://drupal.org/project/wysiwyg

Dev version

Let's build!

  • campwidgets.info
  • campwidgets.module

campwidgets.info

name = Camp Widgets
description = Adds widgets to CKEditor

core = 7.x

dependencies[] = wysiwyg

campwidgets.module

hook_wysiwyg_plugin() or hook_ckeditor_plugin()

Creating Our Own CKEditor Widget

A simple callout

Task: build a CKEditor Plugin that includes a widget.

Dependencies

  • CKEditor 4 Library ✔
  • CKEditor 4 Widget Plugin ✔
  • CKEditor or WYSIWYG module ✔

Let's build!

The Drupal-y bits

campwidgets.info

(From earlier)

campwidgets.module

hook_wysiwyg_plugin() or hook_ckeditor_plugin()

CKEditor API

plugins/callout/plugin.js

added a callout/plugin.js file

CKEditor docs

CKEDITOR.plugins-method-add

CKEDITOR.pluginDefinition

CKEDITOR.plugins.widget.repository-method-add

CKEDITOR.plugins.widget.definition

Add your button icon

added an icons/callout.png file

16x16px and 32x32px for hidpi

Add default callout styling

  • Create callout.css
  • Load callout.css file in the WYSIWYG
  • Load callout.css file on the front end

Create callout.css

Adds callout.css to the root of the module directory

Load CSS in the WYSIWYG

hook_ckeditor_settings_alter() or hook_wysiwyg_editor_settings_alter()

Let's add to our hook implementation from earlier

Load CSS in the front end

Add to stylesheets in campwidgets.info

name = Camp Widgets
description = Adds widgets to CKEditor

core = 7.x

dependencies[] = wysiwyg

stylesheets[all][] = callout.css

Demo

Full plugin.js


CKEDITOR.plugins.add('callout', {

  requires: 'widget',

  icons: 'callout',

  init: function( editor ) {

    editor.widgets.add( 'callout', {

        button: 'Create a simple callout',

        template:
            '
' + '

Callout!

' + '
', editables: { content: { selector: '.callout-content', allowedContent: 'p br ul ol li strong em' } }, allowedContent: 'div(!callout); div(!callout-content);', // Required content for the widget to function. If editor does not // support this, the button does not function. requiredContent: 'div(callout)', upcast: function( element ) { return element.name == 'div' && element.hasClass( 'callout' ); }, }); } });

One more thing...

Creating a dialog for our widget

  • Set width
  • Set alignment via a class "align-right", etc
CKEditor dialog with width and alignment fields

Register the dialog window


CKEDITOR.plugins.add('callout', {
  //Existing code...

  init: function( editor ) {

    editor.widgets.add( 'callout', {
    		// Existing code...

    		// Associate the callout dialog with this widget.
        dialog: 'callout',

    });

    // Register the dialog and its path.
    CKEDITOR.dialog.add( 'callout', this.path + 'dialogs/callout.js' );
  }
});
									
added dialog directory and inside callout.js file

Create dialog callout.js file

Styling - callout.css


@media all and (min-width: 600px) {
    .callout.align-right {
        float: right;
    }
    .callout.align-left {
        float: left;
    }
}
.callout.align-center {
    margin-left: auto;
    margin-right: auto;
}
									

Alter allowedContent

We don't want our new properties filtered out


editor.widgets.add( 'callout', {
    // Code before...

    // ACF allowed content.
    allowedContent: 'div(!callout,align-left,align-right,align-center){width});'+
        'div(!callout-content);',

    // Code after...
});
    							

Widget init method


editor.widgets.add( 'callout', {
    // Code before...

    init: function() {
        var width = this.element.getStyle( 'width' );
        if ( width )
            this.setData( 'width', width );
        if ( this.element.hasClass( 'align-left' ) )
            this.setData( 'align', 'left' );
        if ( this.element.hasClass( 'align-right' ) )
            this.setData( 'align', 'right' );
        if ( this.element.hasClass( 'align-center' ) )
            this.setData( 'align', 'center' );
    }
});
    							

Widget data method


editor.widgets.add( 'callout', {
    // Code before...

    data: function() {
        if ( this.data.width == '' )
            this.element.removeStyle( 'width' );
        else
            this.element.setStyle( 'width', this.data.width );

        this.element.removeClass( 'align-left' );
        this.element.removeClass( 'align-right' );
        this.element.removeClass( 'align-center' );
        if ( this.data.align )
            this.element.addClass( 'align-' + this.data.align );
    }
});
    							

Final plugin.js code


CKEDITOR.plugins.add('callout', {

  // CKEditor plugin dependencies.
  requires: 'widget',

  // Icon file name. callout.png.
  icons: 'callout',

  // Plugin initialization method.
  init: function( editor ) {

    // Register our callout widget.
    editor.widgets.add( 'callout', {
        // Button text on hover state.
        button: 'Create a simple callout',

        // The widget template.
        template:
            '
' + '

Callout!

' + '
', // Widget editable areas. editables: { content: { selector: '.callout-content', allowedContent: 'p br ul ol li strong em' } }, // ACF allowed content. allowedContent: 'div(!callout,align-left,align-right,align-center){width});'+ 'div(!callout-content);', // Required content for the widget to function. If editor does not // support this, the button does not function. requiredContent: 'div(callout)', // Function to determin if an element is a widget. upcast: function( element ) { return element.name == 'div' && element.hasClass( 'callout' ); }, dialog: 'callout', init: function() { var width = this.element.getStyle( 'width' ); if ( width ) this.setData( 'width', width ); if ( this.element.hasClass( 'align-left' ) ) this.setData( 'align', 'left' ); if ( this.element.hasClass( 'align-right' ) ) this.setData( 'align', 'right' ); if ( this.element.hasClass( 'align-center' ) ) this.setData( 'align', 'center' ); }, data: function() { if ( this.data.width == '' ) this.element.removeStyle( 'width' ); else this.element.setStyle( 'width', this.data.width ); this.element.removeClass( 'align-left' ); this.element.removeClass( 'align-right' ); this.element.removeClass( 'align-center' ); if ( this.data.align ) this.element.addClass( 'align-' + this.data.align ); } }); CKEDITOR.dialog.add( 'callout', this.path + 'dialogs/callout.js' ); } });

Another thing...

Widget-based styles

Widget-based styles

CKEditor styles dropdown for a widget

Creation of widget-only style options in the "Styles" dropdown

CSS - callout.css


.primary {;
  font-size: 1.2em;
  line-height: 1.3em;
  font-weight: 400;
  background-color: #F2F2F2;
  border: none;
  border-top: 1px solid #DFDFDF;
  border-bottom: 1px solid #DFDFDF;
}

.callout.fancy {
    padding: 8px;
    margin: 10px;
    background: #eee;
    border-radius: 8px;
    border: 1px solid #ddd;
    box-shadow: 0 1px 1px #fff inset, 0 -1px 0px #ccc inset;
    max-width: 100%;
    min-width: 200px;
}

.fancy .callout-content {
    box-shadow: 0 1px 1px #ddd inset;
    border: 1px solid #cccccc;
    border-radius: 5px;
    background: #fff;
    padding: 8px;
}
									

Modify CKEditor styleSet array


/**
 * Implements hook_wysiwyg_editor_settings_alter().
 */
function campwidgets_wysiwyg_editor_settings_alter(&$settings, $context) {
  // Check if the editor is CKEditor.
  if ($context['profile']->editor == 'ckeditor') {

    $settings['layoutmanager_loadbootstrap'] = TRUE;

    // Get module path.
    $module_location = $GLOBALS['base_path'] . drupal_get_path('module', 'campwidgets');

    // Include custom css files.
    $settings['contentsCss'][] = $module_location . '/callout.css';

    // Define fancy style set.
    $style_fancy = array(
      'name' => 'Fancy',
      'type' => 'widget',
      'widget' => 'callout',
      'attributes' => array(
        'class' => 'fancy'
      )
    );

    // Define primary style set.
    $style_primary = array(
      'name' => 'Primary',
      'type' => 'widget',
      'widget' => 'callout',
      'attributes' => array(
        'class' => 'primary'
      )
    );

    $settings['stylesSet'][] = $style_fancy;
    $settings['stylesSet'][] = $style_primary;
  }
}
									

CKEditor docs: widget styles

Resources

Questions?

Feedback

https://2015.drupalcorn.org/evaluation