/**
* Note TinyMCE Placeholder Plugin
*/
tinymce.PluginManager.add( 'note_placeholder', function( editor ) {
'use strict';
var DOM = tinymce.DOM,
Factory = tinymce.ui.Factory,
prev_node, // Reference to the previous node in the editor
$prev_node,
$ = jQuery,
api = wp.customize, // Customizer API
NotePreview = api.NotePreview, // NotePreview
$el = $( editor.getElement() ),
data_key = 'note',
content_class = 'note-content',
placeholder_class = 'note-has-placeholder',
mixed_content_class = 'note-has-mixed-content',// Some placeholder content, some normal content
placeholder_el_class = 'note-placeholder',
placeholder_el_parent_class = 'note-placeholder-parent',
$el_parent = $el.parent(),
wp_media_active = false, // Flag to determine if wp.media modal was open
media_panel,
toolbar;
/*
* TinyMCE Editor Events
*/
// Preinit
editor.on( 'preinit', function() {
var note_type = editor.getParam( 'note_type' );
// Only on media editors
if ( note_type && note_type === 'media' && editor.settings.hasOwnProperty( 'media_blocks' ) ) {
// Create the panel (no items)
media_panel = Factory.create( {
type: 'panel',
layout: 'flow',
classes: 'insert-panel note-insert-panel media-insert-panel note-media-insert-panel',
ariaRoot: true,
ariaRemember: true,
items: []
} );
/*
* This function sets the panel's position in the DOM.
*/
media_panel.setPosition = function() {
var insert_el = this.getEl(); // Insert element
// Set the styles on the insert element
DOM.setStyles( insert_el, {
//'left': parent_pos.x,
//'top': parent_pos.y,
//'width': $parent[0].offsetWidth,
//'height': $parent[0].offsetHeight
} );
// Return this for chaining
return this;
};
}
} );
// Init
editor.on( 'init', function( event ) {
var note_type = editor.getParam( 'note_type' );
// Note Placeholder
setupNotePlaceholder();
// Remove the note-placeholder data attributes
/*$el.find( '*[data-note-placeholder]' ).each( function() {
// Remove the note-placeholder data attribute
$( this ).removeAttr( 'data-note-placeholder' );
} );*/
// Only on media editors
if ( note_type && note_type === 'media' ) {
// If the WordPress plugin has been initialized and we can create a toolbar
if ( editor.wp && editor.wp._createToolbar ) {
// Render the panel to the editor
media_panel.renderTo( $el_parent[0] );
// Hide the media panel
media_panel.hide();
/*
* Create the toolbar.
*
* Because the WordPress TinyMCE plugin renders the toolbar to the DOM for us,
* we need to add it after the panel is rendered so that we can append it to our
* panel 'body' element.
*/
// Create the toolbar
toolbar = editor.wp._createToolbar( editor.settings.media_blocks );
// Add the toolbar to our panel
media_panel.add( toolbar );
// Append the toolbar to our panel in the DOM (grab the DOMQuery reference)
toolbar.$el.appendTo( media_panel.getEl( 'body' ) );
// Note Placeholder
if ( $el.hasClass( placeholder_class ) ) {
// Remove all content
editor.setContent( '' );
// Add CSS class to parent
$el_parent.addClass( 'has-media-placeholder' );
// Show the toolbar (WordPress hides it by default)
toolbar.show();
// Show the media panel
media_panel.show();
}
}
}
} );
// Editor NodeChange
editor.on( 'NodeChange', function( event ) {
var node = editor.selection.getNode(),
$node = $( node ),
node_editor_id = $node.parents( '.editor' ).attr( 'id' ),
text = $node.text(),
note_data = $node.data( data_key ),
placeholder = ( note_data && note_data.hasOwnProperty( 'placeholder' ) ) ? note_data.placeholder : false,
note_type = editor.getParam( 'note_type' );
// Note Placeholder element
if ( node_editor_id === editor.id && $node.hasClass( placeholder_el_class ) && text === placeholder ) {
// Set flag to stop Note Widget updates
editor.note.placeholder_el = true;
editor.note.prevent_widget_update = true;
// Remove the placeholder CSS
$node.removeClass( placeholder_el_class ).parentsUntil( '.' + content_class ).removeClass( placeholder_el_parent_class );
// Remove the placeholder content
$node.html( '<br />' ); // Set a break to preserve the element editing capability, TinyMCE will handle the rest for us
}
// Otherwise we have normal element
else {
// Reset flag to stop Note Widget updates
editor.note.placeholder_el = false;
editor.note.prevent_widget_update = false;
}
// Determine if this node is different than the previous (ignoring TinyMCE paste bin element)
if ( node_editor_id === editor.id && $node.attr( 'id' ) !== 'mcepastebin' && ! compareNodes( node, prev_node ) ) {
// Determine if previous node is empty and reset the placeholder
if ( prev_node && $prev_node.length && ! $prev_node.text() && ! $prev_node.has( 'img' ).length ) {
// Previous node placeholder
note_data = $prev_node.data( data_key );
placeholder = ( note_data && note_data.hasOwnProperty( 'placeholder' ) ) ? note_data.placeholder : false;
// If we have placeholder data
if ( placeholder ) {
// Reset placeholder
$prev_node.html( DOM.decode( placeholder ) ).addClass( placeholder_el_class ).parentsUntil( '.' + content_class ).addClass( placeholder_el_parent_class );
}
}
// Update the previous node
prev_node = node;
$prev_node = $( prev_node );
}
// Reset the wp.media flag
if ( wp_media_active ) {
wp_media_active = false;
}
// Only on media editors
if ( note_type && note_type === 'media' ) {
// Adjust the position of the media panel
media_panel.setPosition();
}
} );
// Editor change & keypress
editor.on( 'change keypress', function( event ) {
// If the editor placeholder element flag is set
if ( editor.note.hasOwnProperty( 'placeholder_el' ) && editor.note.placeholder_el ) {
// Reset the placeholder element flag
editor.note.placeholder_el = false;
// Reset the widget update flag
editor.note.prevent_widget_update = false;
}
} );
// TODO: Editor undo/redo (placeholder doesn't always return on undo/redo events)
/*editor.on( 'undo', function( event ) {
var node = editor.selection.getNode(),
$node = $( node );
// Setup Note Placeholder (setTimeout ensures default placeholder data exists before re-init)
setTimeout( function() {
setupNotePlaceholder();
}, 20 );
} );*/
// Editor paste (post-process)
editor.on( 'PastePostProcess', function( event ) {
// If the editor placeholder element flag is set
if ( editor.note.hasOwnProperty( 'placeholder_el' ) && editor.note.placeholder_el ) {
// Reset the placeholder element flag
editor.note.placeholder_el = false;
// Reset the widget update flag
editor.note.prevent_widget_update = false;
}
} );
// Editor wpLoadImageForm
editor.on( 'wpLoadImageForm', function() {
// If we don't have focus
if ( ! DOM.hasClass( editor.getBody(), 'mce-edit-focus' ) ) {
// Focus the editor first (skip focusing and just set the active editor)
editor.focus( true );
// Since we're skipping the DOM focusing, we need to set the global wpActiveEditor ID, which is used as a fallback when WordPress is determining the active TinyMCE editor (@see https://github.com/WordPress/WordPress/blob/4.5-branch/wp-includes/js/tinymce/plugins/wordpress/plugin.js#L89)
window.wpActiveEditor = editor.id;
}
// Set the wp.media flag
wp_media_active = true;
} );
// Editor wpLoadImageForm once
editor.once( 'wpLoadImageForm', function( event ) {
// Listen for the close event on the frame
event.frame.on( 'close', function() {
var node = editor.selection.getNode(),
$node = $( node ),
node_editor_id = $node.parents( '.editor' ).attr( 'id' ),
text = $node.text(),
note_data = $node.data( data_key ),
placeholder = ( note_data && note_data.hasOwnProperty( 'placeholder' ) ) ? note_data.placeholder : false;
// Note Placeholder element
if ( node_editor_id === editor.id && $node.hasClass( placeholder_el_class ) && text === placeholder ) {
// Remove the placeholder CSS
$node.removeClass( placeholder_el_class ).parentsUntil( '.' + content_class ).removeClass( placeholder_el_parent_class );
// Remove the placeholder content
$node.html( '<br />' ); // Set a break to preserve the element editing capability, TinyMCE will handle the rest for us
// Focus the editor
editor.focus();
}
} );
// Listen for the insert event on the frame
event.frame.on( 'insert', function() {
// Remove the placeholder
editor.dom.remove( editor.dom.select( '.note-placeholder' ) );
// Remove CSS class from parent
$el_parent.removeClass( 'has-media-placeholder' );
// Hide the media panel
if ( media_panel ) {
media_panel.hide();
}
} );
} );
// Editor note-editor-focus
editor.on( 'note-editor-focus', function() {
var node = editor.selection.getNode(),
$node = $( node ),
node_editor_id = $node.parents( '.editor' ).attr( 'id' ),
text = $node.text(),
note_data = $node.data( data_key ),
placeholder = ( note_data && note_data.hasOwnProperty( 'placeholder' ) ) ? note_data.placeholder : false;
// Note Placeholder element
if ( node_editor_id === editor.id && $node.hasClass( placeholder_el_class ) && text === placeholder ) {
// Set flag to stop Note Widget updates
editor.note.placeholder_el = true;
editor.note.prevent_widget_update = true;
// Remove the placeholder CSS
$node.removeClass( placeholder_el_class ).parentsUntil( '.' + content_class ).removeClass( placeholder_el_parent_class );
// Remove the placeholder content
$node.html( '<br />' ); // Set a break to preserve the element editing capability, TinyMCE will handle the rest for us
}
} );
// Editor blur
editor.on( 'blur', function( event ) {
var note_data,
placeholder,
note_type = editor.getParam( 'note_type' ),
$body = $( editor.getBody() );
// Determine if previous node is empty and reset the placeholder
if ( prev_node && $prev_node.length && ! $prev_node.text() && ! $prev_node.has( 'img' ).length ) {
note_data = $prev_node.data( data_key );
placeholder = ( note_data && note_data.hasOwnProperty( 'placeholder' ) ) ? note_data.placeholder : false;
// If we have placeholder data
if ( placeholder ) {
// Reset placeholder (setTimeout fixes a bug where the previous node would remain empty when switching focus between editors)
setTimeout( function() {
// Reset the placeholder text
$prev_node.html( DOM.decode( placeholder ) ).addClass( placeholder_el_class ).parentsUntil( '.' + content_class ).addClass( placeholder_el_parent_class );
}, 10 );
// Note Widget update (setTimeout ensures placeholder element has finished inserting into element)
setTimeout( function() {
var content = editor.getContent(),
// Deep copy
data = $.extend( true, editor.note.widget_data, { widget: { content: content } } );
// Trigger a Note Widget update event (after placeholder data has been put back into element)
NotePreview.preview.send( 'note-widget-update', data );
// Update the previous content reference
editor.note.prev_content = content;
}, 100 );
}
}
// Only on media editors
if ( note_type && note_type === 'media' && ( ! $body.text() && ! $body.has( 'img' ).length ) ) {
// Remove all content
editor.setContent( '' );
// Add CSS class to parent
$el_parent.addClass( 'has-media-placeholder' );
// Show the media panel
media_panel.show();
}
} );
/**********************
* Internal Functions *
**********************/
/**
* This function sets up Note Placeholder logic.
*/
function setupNotePlaceholder() {
if ( $el.hasClass( placeholder_class ) ) {
// Placeholder content only
if ( ! $el.hasClass( mixed_content_class ) ) {
// Loop through nodes // TODO: Optimize this call if possible (can we loop through children() only?)
$el.find( '*:not([data-note-placeholder="false"])' ).each( function() {
var $this = $( this );
// Add the Note Placeholder CSS classes
$this.addClass( placeholder_el_class ).parentsUntil( '.' + content_class ).addClass( placeholder_el_parent_class );
// Add the Note Placeholder data attribute (set to current content value)
$this.data( data_key, { placeholder: DOM.encode( $this.text() ) } );
} );
}
// Mixed content
else {
// Loop through placeholder nodes // TODO: Optimize this call if possible (can we loop through children() only?)
$el.find( '.' + placeholder_el_class ).each( function() {
var $this = $( this );
// Add the Note Placeholder CSS classes
$this.parentsUntil( '.' + content_class ).addClass( placeholder_el_parent_class );
// Add the Note Placeholder data attribute (set to current content value)
$this.data( data_key, { placeholder: DOM.encode( $this.text() ) } );
} );
}
}
}
/**
* Compares two nodes and checks if it's attributes and styles matches.
* This doesn't compare classes as items since their order is significant.
* We've modified this function to also compare the content of the nodes.
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*
* @param node1
* @param node2
* @returns {boolean}
*/
function compareNodes(node1, node2) {
// Not the same element (simple check first)
if (node1 && node2 && node1 !== node2) {
return false;
}
/**
* Returns all the nodes attributes excluding internal ones, styles and classes.
*
* @private
* @param {Node} node Node to get attributes from.
* @return {Object} Name/value object with attributes and attribute values.
*/
function getAttribs(node) {
var attribs = {};
tinymce.util.Tools.each(DOM.getAttribs(node), function(attr) {
var name = attr.nodeName.toLowerCase();
// Don't compare internal attributes or style
if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') {
attribs[name] = DOM.getAttrib(node, name);
}
});
return attribs;
}
/**
* Compares two objects checks if it's key + value exists in the other one.
*
* @private
* @param {Object} obj1 First object to compare.
* @param {Object} obj2 Second object to compare.
* @return {boolean} True/false if the objects matches or not.
*/
function compareObjects(obj1, obj2) {
var value, name;
for (name in obj1) {
// Obj1 has item obj2 doesn't have
if (obj1.hasOwnProperty(name)) {
value = obj2[name];
// Obj2 doesn't have obj1 item
if (typeof value == "undefined") {
return false;
}
// Obj2 item has a different value
if (obj1[name] != value) {
return false;
}
// Delete similar value
delete obj2[name];
}
}
// Check if obj 2 has something obj 1 doesn't have
for (name in obj2) {
// Obj2 has item obj1 doesn't have
if (obj2.hasOwnProperty(name)) {
return false;
}
}
return true;
}
// Attribs are not the same
if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
return false;
}
// Styles are not the same
if (!compareObjects(DOM.parseStyle(DOM.getAttrib(node1, 'style')), DOM.parseStyle(DOM.getAttrib(node2, 'style')))) {
return false;
}
// Content (innerHTML) is not the same
if( node1 && node2 && DOM.encode( node1.innerHTML ) !== DOM.encode( node2.innerHTML ) ) {
return false;
}
return !tinymce.dom.BookmarkManager.isBookmarkNode(node1) && !tinymce.dom.BookmarkManager.isBookmarkNode(node2);
}
} );