( function( window, evo, shortcode, $ )
{
'use strict';
var views = {},
instances = {};
/**
* evo_views
*
* A set of utilities that simplifies adding custom UI within a TinyMCE editor.
* At its core, it serves as a series of converters, transforming text to a
* custom UI, and back again.
*/
evo.views = {
/**
* Registers a new view type.
*
* @param {String} type The view type.
* @param {Object} extend An object to extend wp.mce.View.prototype with.
*/
register: function( type, options ) {
views[ type ] = evo.View.extend( $.extend( options, { type: type } ) );
},
/**
* Unregisters a view type.
*
* @param {String} type The view type.
*/
unregister: function( type ) {
delete views[ type ];
},
/**
* Returns the settings of a view type.
*
* @param {String} type The view type.
*
* @return {Function} The view constructor.
*/
get: function( type ) {
return views[ type ];
},
/**
* Unbinds all view nodes.
* Runs before removing all view nodes from the DOM.
*/
unbind: function() {
$.each( instances, function( index, instance ) {
instance.unbind();
} );
},
/**
* Scans a given string for each view's pattern,
* replacing any matches with markers,
* and creates a new instance for every match.
*
* @param {String} content The string to scan.
*
* @return {String} The string with markers.
*/
setMarkers: function( content ) {
var pieces = [ { content: content } ],
self = this,
instance, current;
$.each( views, function( type, view ) {
current = pieces.slice();
pieces = [];
$.each( current, function( index, piece ) {
var remaining = piece.content,
result, text;
// Ignore processed pieces, but retain their location.
if( piece.processed )
{
pieces.push( piece );
return;
}
// Iterate through the string progressively matching views
// and slicing the string as we go.
while( remaining && ( result = view.prototype.match( remaining ) ) )
{
// Any text before the match becomes an unprocessed piece.
if( result.index )
{
pieces.push( { content: remaining.substring( 0, result.index ) } );
}
instance = self.createInstance( type, result.content, result.options );
text = instance.loader ? '.' : instance.text;
// Add the processed piece for the match.
pieces.push( {
content: instance.ignore ? text : '<span data-evo-view-marker="' + instance.encodedText + '">' + text + '</span>',
processed: true
} );
// Update the remaining content.
remaining = remaining.slice( result.index + result.content.length );
}
// There are no additional matches.
// If any content remains, add it as an unprocessed piece.
if( remaining )
{
pieces.push( { content: remaining } );
}
} );
} );
var arr = [];
$.each( pieces, function( index, piece ) {
return arr.push( piece.content );
});
content = arr.join( '' );
return content;
},
/**
* Create a view instance.
*
* @param {String} type The view type.
* @param {String} text The textual representation of the view.
* @param {Object} options Options.
* @param {Boolean} force Recreate the instance. Optional.
*
* @return {wp.mce.View} The view instance.
*/
createInstance: function( type, text, options, force ) {
var View = this.get( type ),
encodedText,
instance;
text = tinymce.DOM.decode( text );
if( ! force )
{
instance = this.getInstance( text );
if( instance )
{
return instance;
}
}
encodedText = encodeURIComponent( text );
options = $.extend( options || {}, {
text: text,
encodedText: encodedText,
renderedHTML: null,
} );
//return evo.instances[ encodedText ] = new View( options );
return instances[ text ] = new View( options );
},
/**
* Get a view instance.
*
* @param {(String|HTMLElement)} object The textual representation of the view or the view node.
*
* @return {wp.mce.View} The view instance or undefined.
*/
getInstance: function( object ) {
if( typeof object === 'string' )
{
return instances[ object ];
}
return instances[ window.decodeURIComponent( $( object ).attr( 'data-evo-view-text' ) ) ];
},
/**
* Given a view node, get the view's text.
*
* @param {HTMLElement} node The view node.
*
* @return {String} The textual representation of the view.
*/
getText: function( node ) {
return decodeURIComponent( $( node ).attr( 'data-evo-view-text' ) || '' );
},
/**
* Renders all view nodes that are not yet rendered.
*
* @param {Boolean} force Rerender all view nodes.
*/
render: function( force ) {
var self = this,
params = [];
$.each( instances, function( index, instance ) {
if( !instance.renderedHTML ) {
params.push( 'tags[]=' + encodeURI( instance.text ) );
}
} );
// Fetch rendered HTML fragments:
if(params.length) {
params = params.join( '&' );
evo.View.prototype.getEditors( function( editor ) {
tinymce.util.XHR.send( {
url: editor.getParam( 'anon_async_url' ) + '?action=render_inlines&type='
+ editor.getParam( 'target_type' )
+ '&id=' + ( editor.getParam( 'target_ID' ) == undefined ? '' : editor.getParam( 'target_ID' ) )
+ ( editor.getParam( 'temp_ID' ) ? '&temp_link_owner_ID=' + editor.getParam( 'temp_ID' ) : '' ),
content_type : 'application/x-www-form-urlencoded',
data: params,
success: function( data ) {
if(data)
{
var returnedTags = tinymce.util.JSON.parse( data );
for( var shortTag in returnedTags )
{
var wrapper = editor.dom.create('div');
var df = editor.dom.createFragment( returnedTags[shortTag] );
wrapper.appendChild( df );
if( shortTag != wrapper.innerHTML )
{
var instance = self.getInstance( shortTag );
instance.renderedHTML = wrapper.innerHTML;
}
}
}
$.each( instances, function( index, instance ) {
instance.render( instance.renderedHTML, force );
} );
}
} );
} );
}
else
{
$.each( instances, function( index, instance ) {
instance.render( instance.renderedHTML, force );
} );
}
},
/**
* Update the text of a given view node.
*
* @param {String} text The new text.
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
* @param {HTMLElement} node The view node to update.
* @param {Boolean} force Recreate the instance. Optional.
*/
update: function( text, editor, node, force ) {
var instance = this.getInstance( node );
if( instance )
{
instance.update( text, editor, node, force );
}
},
/**
* Renders any editing interface based on the view type.
*
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
* @param {HTMLElement} node The view node to edit.
*/
edit: function( editor, node ) {
var instance = this.getInstance( node );
if( instance && instance.edit )
{
instance.edit( instance.text, function( text, force ) {
instance.update( text, editor, node, force );
} );
}
},
/**
* Remove a given view node from the DOM.
*
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
* @param {HTMLElement} node The view node to remove.
*/
remove: function( editor, node ) {
var instance = this.getInstance( node );
if( instance )
{
instance.remove( editor, node );
}
}
};
//evo.View.extend = Backbone.View.extend;
/**
* A Backbone-like View constructor intended for use when rendering a TinyMCE View.
* The main difference is that the TinyMCE View is not tied to a particular DOM node.
*
* @param {Object} options Options.
*/
evo.View = function( options ) {
$.extend( this, options );
this.initialize();
};
evo.View.extend = function( options ) {
var subView = function( options ) {
evo.View.call( this, options );
}
subView.prototype = Object.create( evo.View.prototype );
subView.prototype.constructor = subView;
for( name in options )
{
subView.prototype[name] = options[name];
}
return subView;
};
$.extend( evo.View.prototype, {
/**
* The content.
*
* @type string Rendered content
*/
content: null,
/**
* Whether or not to display a loader.
*
* @type boolean True to display loader, false otherwise
*/
loader: true,
/**
* Runs after the view instance is created.
*/
initialize: function() {},
/**
* Retuns the content to render in the view node.
*
* @return {*}
*/
getContent: function( editor ) {
return this.content;
},
/**
* Renders all view nodes tied to this view instance that are not yet rendered.
*
* @param string The content to render. Optional.
* @param boolean Rerender all view nodes tied to this view instance. Optional.
*/
render: function( content, force ) {
var self = this;
if( content != null )
{
this.content = content;
}
content = this.getContent();
// If there's nothing to render and no loader needs to be shown, stop.
if( ! this.loader && ! content )
{
return;
}
// We're about to rerender all views of this instance, so unbind rendered views.
force && this.unbind();
// Replace any left over markers.
this.replaceMarkers();
if( content )
{
this.setContent( content, function( editor, node, contentNode ) {
$( node ).data( 'rendered', true );
self.bindNode.call( self, editor, node, contentNode );
}, force ? null : false );
}
},
/**
* Binds a given node after its content is added to the DOM.
*/
bindNode: function() {},
/**
* Unbinds a given node before its content is removed from the DOM.
*/
unbindNode: function() {},
/**
* Unbinds all view nodes tied to this view instance.
* Runs before their content is removed from the DOM.
*/
unbind: function() {
var self = this;
this.getNodes( function( editor, node, contentNode ) {
self.unbindNode.call( self, editor, node, contentNode );
$( node ).trigger( 'evo-view-unbind' );
}, true );
},
/**
* Gets all the TinyMCE editor instances that support views.
*
* @param {Function} callback A callback.
*/
getEditors: function( callback ) {
$.each( tinymce.editors, function( index, editor ) {
if( editor.plugins.evo_view )
{
callback.call( this, editor );
}
}, this );
},
/**
* Gets all view nodes tied to this view instance.
*
* @param {Function} callback A callback.
* @param {Boolean} rendered Get (un)rendered view nodes. Optional.
*/
getNodes: function( callback, rendered ) {
var self = this;
this.getEditors( function( editor ) {
$( editor.getBody() )
.find( '[data-evo-view-text="' + self.encodedText + '"]' )
.filter( function() {
var data;
if( rendered == null )
{
return true;
}
data = $( this ).data( 'rendered' ) === true;
return rendered ? data : ! data;
} )
.each( function() {
callback.call( self, editor, this, $( this ).find( '.evo-view-content' ).get( 0 ) );
} );
} );
},
/**
* Gets all marker nodes tied to this view instance.
*
* @param {Function} callback A callback.
*/
getMarkers: function( callback ) {
var encodedText = this.encodedText;
this.getEditors( function( editor ) {
var self = this;
$( editor.getBody() )
.find( '[data-evo-view-marker="' + encodedText + '"]' )
.each( function() {
callback.call( self, editor, this );
} );
} );
},
/**
* Marker text
*/
markerText: '<div class="evo-view-wrap" data-evo-view-text="%encodedText%" data-evo-view-type="%viewType%">' +
'<div class="evo-view-selection-before"></div>' +
'<div class="evo-view-body" contenteditable="false">' +
'<div class="evo-view-content evo-view-type-%viewType%"></div>' +
'</div>' +
'<div class="evo-view-selection-after"></div>' +
'</div>',
/**
* Replaces all marker nodes tied to this view instance.
*/
replaceMarkers: function() {
var self = this;
this.getMarkers( function( editor, node ) {
var selected = node === editor.selection.getNode(), $viewNode;
if( ! self.loader && $( node ).text() !== self.text )
{
editor.dom.setAttrib( node, 'data-evo-view-marker', null );
return;
}
var markerText = self.markerText.replace( /%encodedText%/g, self.encodedText ).replace( /%viewType%/g, self.type );
$viewNode = editor.$( markerText );
editor.$( node ).replaceWith( $viewNode );
if( selected )
{
editor.evo.setViewCursor( false, $viewNode[0] );
}
} );
},
/**
* Removes all marker nodes tied to this view instance.
*/
removeMarkers: function() {
this.getMarkers( function( editor, node ) {
editor.dom.setAttrib( node, 'data-evo-view-marker', null );
} );
},
/**
* Sets the content for all view nodes tied to this view instance.
*
* @param {*} content The content to set.
* @param {Function} callback A callback. Optional.
* @param {Boolean} rendered Only set for (un)rendered nodes. Optional.
*/
setContent: function( content, callback, rendered ) {
if( $.type( content ) === 'object' && content.body.indexOf( '<script' ) !== -1 )
{
this.setIframes( content.head || '', content.body, callback, rendered );
}
else if( $.type( content ) === 'string' && content.indexOf( '<script' ) !== -1 )
{
this.setIframes( '', content, callback, rendered );
}
else if( content == this.text )
{ // Rendered content is the same as inline:
this.getNodes( function( editor, node, contentNode ) {
node.replaceWith( this.text );
}, rendered );
}
else
{
this.getNodes( function( editor, node, contentNode ) {
content = content.body || content;
if( content.indexOf( '<iframe' ) !== -1 )
{
content += '<div class="evo-view-overlay"></div>';
}
contentNode.innerHTML = '';
contentNode.appendChild( typeof content === 'string' ? editor.dom.createFragment( content ) : content );
callback && callback.call( this, editor, node, contentNode );
}, rendered );
}
},
/**
* Sets the content in an iframe for all view nodes tied to this view instance.
*
* @param {String} head HTML string to be added to the head of the document.
* @param {String} body HTML string to be added to the body of the document.
* @param {Function} callback A callback. Optional.
* @param {Boolean} rendered Only set for (un)rendered nodes. Optional.
*/
setIframes: function( head, body, callback, rendered ) {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
self = this;
this.getNodes( function( editor, node, contentNode ) {
var dom = editor.dom,
styles = '',
bodyClasses = editor.getBody().className || '',
editorHead = editor.getDoc().getElementsByTagName( 'head' )[0];
tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), function( index, link ) {
if( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1
&& link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 )
{
styles += dom.getOuterHTML( link );
}
} );
if( self.iframeHeight )
{
dom.add( contentNode, 'div', { style: {
width: '100%',
height: self.iframeHeight
} } );
}
// Seems the browsers need a bit of time to insert/set the view nodes,
// or the iframe will fail especially when switching Text => Visual.
setTimeout( function() {
var iframe, iframeDoc, observer, i, block;
contentNode.innerHTML = '';
iframe = dom.add( contentNode, 'iframe', {
/* jshint scripturl: true */
src: tinymce.Env.ie ? 'javascript:""' : '',
frameBorder: '0',
allowTransparency: 'true',
scrolling: 'no',
'class': 'evo-view-sandbox',
style: {
width: '100%',
display: 'block'
},
height: self.iframeHeight
} );
dom.add( contentNode, 'div', { 'class': 'evo-view-overlay' } );
iframeDoc = iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(
'<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
head +
styles +
'<style>' +
'html {' +
'background: transparent;' +
'padding: 0;' +
'margin: 0;' +
'}' +
'body#evo-view-iframe-sandbox {' +
'background: transparent;' +
'padding: 1px 0 !important;' +
'margin: -1px 0 0 !important;' +
'}' +
'body#evo-view-iframe-sandbox:before,' +
'body#evo-view-iframe-sandbox:after {' +
'display: none;' +
'content: "";' +
'}' +
'</style>' +
'</head>' +
'<body id="evo-view-iframe-sandbox" class="' + bodyClasses + '">' +
body +
'</body>' +
'</html>'
);
iframeDoc.close();
function resize()
{
var $iframe;
if( block )
{
return;
}
// Make sure the iframe still exists.
if( iframe.contentWindow )
{
$iframe = $( iframe );
self.iframeHeight = $( iframeDoc.body ).height();
if( $iframe.height() !== self.iframeHeight )
{
$iframe.height( self.iframeHeight );
editor.nodeChanged();
}
}
}
if( self.iframeHeight )
{
block = true;
setTimeout( function() {
block = false;
resize();
}, 3000 );
}
$( iframe.contentWindow ).on( 'load', resize );
if ( MutationObserver )
{
observer = new MutationObserver( _.debounce( resize, 100 ) );
observer.observe( iframeDoc.body, {
attributes: true,
childList: true,
subtree: true
} );
$( node ).one( 'evo-view-unbind', function() {
observer.disconnect();
} );
}
else
{
for ( i = 1; i < 6; i++ )
{
setTimeout( resize, i * 700 );
}
}
function classChange()
{
iframeDoc.body.className = editor.getBody().className;
}
editor.on( 'evo-body-class-change', classChange );
$( node ).one( 'evo-view-unbind', function() {
editor.off( 'evo-body-class-change', classChange );
} );
callback && callback.call( self, editor, node, contentNode );
}, 50 );
}, rendered );
},
/**
* Sets a loader for all view nodes tied to this view instance.
*/
setLoader: function() {
this.setContent(
'<div class="loading-placeholder">' +
'<div class="dashicons dashicons-admin-media"></div>' +
'<div class="evo-view-loading"><ins></ins></div>' +
'</div>'
);
},
/**
* Sets an error for all view nodes tied to this view instance.
*
* @param {String} message The error message to set.
* @param {String} dashicon A dashicon ID. Optional. {@link https://developer.wordpress.org/resource/dashicons/}
*/
setError: function( message, dashicon ) {
this.setContent(
'<div class="evo-view-error">' +
'<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
'<p>' + message + '</p>' +
'</div>'
);
},
/**
* Tries to find a text match in a given string.
*
* @param {String} content The string to scan.
*
* @return {Object}
*/
match: function( content ) {
var match = shortcode.next( this.type, content );
if( match )
{
return {
index: match.index,
content: match.content,
options: {
shortcode: match.shortcode
}
};
}
},
/**
* Update the text of a given view node.
*
* @param {String} text The new text.
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
* @param {HTMLElement} node The view node to update.
* @param {Boolean} force Recreate the instance. Optional.
*/
update: function( text, editor, node, force ) {
$.each( views, function( key, view ) {
var match = view.prototype.match( text );
if( match )
{
$( node ).data( 'rendered', false );
editor.dom.setAttrib( node, 'data-evo-view-text', encodeURIComponent( text ) );
views.createInstance( type, text, match.options, force ).render();
editor.focus();
return false;
}
} );
},
/**
* Remove a given view node from the DOM.
*
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
* @param {HTMLElement} node The view node to remove.
*/
remove: function( editor, node ) {
this.unbindNode.call( this, editor, node, $( node ).find( '.evo-view-content' ).get( 0 ) );
$( node ).trigger( 'evo-view-unbind' );
editor.dom.remove( node );
editor.focus();
}
} );
//} )( window, window.wp, window.wp.shortcode, window.jQuery );
} )( window, window.evo, window.evo.shortcode, window.jQuery );
/*
* The b2evolution core TinyMCE views.
* Views for the shortags
*/
( function( window, views, $ )
{
var base, gallery, av, embed, image,
schema, parser, serializer;
function verifyHTML( string )
{
var settings = {};
if( ! window.tinymce )
{
return string.replace( /<[^>]+>/g, '' );
}
if( ! string || ( string.indexOf( '<' ) === -1 && string.indexOf( '>' ) === -1 ) )
{
return string;
}
schema = schema || new window.tinymce.html.Schema( settings );
parser = parser || new window.tinymce.html.DomParser( settings, schema );
serializer = serializer || new window.tinymce.html.Serializer( settings, schema );
return serializer.serialize( parser.parse( string, { forced_root_block: false } ) );
}
base = {
loader: true,
};
image = $.extend( {}, base, {
initialize: function() {
var self = this;
var params = [];
self.renderedHTML = null;
}
} );
views.register( 'image', image );
var extended_image_params = $.extend( {}, image, {
markerText: '<span class="evo-view-wrap" data-evo-view-text="%encodedText%" data-evo-view-type="%viewType%">' +
'<span class="evo-view-selection-before"> </span>' +
'<span class="evo-view-body" contenteditable="false">' +
'<span class="evo-view-content evo-view-type-%viewType%"></span>' +
'</span>' +
'<span class="evo-view-selection-after"> </span>' +
'</span>',
} );
views.register( 'thumbnail', extended_image_params );
views.register( 'inline', extended_image_params );
// Tags for plugin "Email Elements":
var email_element_params = $.extend( {}, image, {
markerText: '<div class="evo-view-wrap" data-evo-view-text="%encodedText%" data-evo-view-type="%viewType%" data-evo-view-plugin-type="email_element">' +
'<div class="evo-view-selection-before"></div>' +
'<div class="evo-view-body" contenteditable="false">' +
'<div class="evo-view-content evo-view-type-%viewType%"></div>' +
'</div>' +
'<div class="evo-view-selection-after"></div>' +
'</div>',
} );
views.register( 'button', email_element_params );
views.register( 'cta', email_element_params );
views.register( 'like', email_element_params );
views.register( 'dislike', email_element_params );
views.register( 'activate', email_element_params );
views.register( 'unsubscribe', email_element_params );
} )( window, window.evo.views, window.jQuery );