/home/arranoyd/magicraft/wp-content/plugins/note/assets/js/note.js
/**
 * Note (Previewer) Core
 */

( function ( wp, $ ) {
	'use strict';

	// Bail if the customizer isn't initialized
	if ( ! wp || ! wp.customize ) {
		return;
	}

	var api = wp.customize, OldPreview, isLinkPreviewable;

	// Note Preview
	api.NotePreview = {
		preview: null, // Instance of the Previewer
		editors: [], // TinyMCE Editors
		editor_config: [],  // TinyMCE Editor configurations
		editor_selectors: [], // TinyMCE Editor selectors
		tinymce: window.tinymce,
		tinyMCE: window.tinyMCE,
		tinymce_config: {},
		note: window.note,
		note_tinymce: window.note_tinymce,
		default_note_template_config_type: 'default',
		widget_settings: ( window.note.hasOwnProperty( 'widgets' ) && window.note.widgets.hasOwnProperty( 'settings' ) ) ? window.note.widgets.settings : false,
		widget_templates: ( window.note.hasOwnProperty( 'widgets' ) && window.note.widgets.hasOwnProperty( 'templates' ) ) ? window.note.widgets.templates : false,
		modal_commands: ( window.note.hasOwnProperty( 'modal_commands' ) ) ? window.note.modal_commands : false,
		modal_command_listeners: {
			// Activate
			activate: [],
			// Deactivate
			deactivate: []
		},
		// Flag to determine if a Note widget is currently active (focused)
		is_widget_focused: false,
		// Flag to determine if a Note widget editor currently has a modal window active (open)
		is_widget_modal_active: false,
		$note_widgets: false,
		$document: false,
        $body: false,
		transition_duration: 400, // CSS transition is 400ms
		// Initialization
		init: function () {
			var self = this;

			// Set TinyMCE Reference
			this.tinymce = window.tinymce;

			// Set the Note Widget jQuery reference
			this.$note_widgets = $( '.note-widget' );

			// Set the document jQuery reference
			this.$document = $( document );

            // Set the document jQuery reference
            this.$body = $( 'body' );

			// When the previewer is active
			this.preview.bind( 'active', function() {
				// Append HTML attributes to Note widgets
				self.$note_widgets.each( function() {
					var $el = $( this );

					// Widget Number
					$el.attr( 'data-widget-number', $el.find( '.widget-number' ).val() );
					// Widget ID
					$el.attr( 'data-widget-id', $el.find( '.widget-id' ).val() );
					// Sidebar Name
					$el.attr( 'data-sidebar-name', $el.find( '.sidebar-name' ).val() );
					// Sidebar ID
					$el.attr( 'data-sidebar-id', $el.find( '.sidebar-id' ).val() );

					// Attempt to keep our theme panel/toolbar visible when the mouse leaves editors but is pressed
					$el.parent().on( 'mouseup', function( event ) {
						if ( event.currentTarget === event.srcElement ) {
							// Was the panel supposed to be visible?
							if ( self.tinymce.activeEditor.theme.panel.visible() ) {
								// Make the panel visible again
								setTimeout( function() {
									self.tinymce.activeEditor.theme.panel.visible( true );
								}, 10 );
							}
						}
					} );
				} );

				// Listen for the "note-widget-edit" event from the Customizer
				self.preview.bind( 'note-widget-edit', function( data ) {
					// Find the correct editor
					var editor = _.find( self.editors, function( editor ) {
						// TODO: Check hasOwnProperty()
						return editor.note.widget_data.widget.id === data.widget.id;
					} ),
						body = ( editor ) ? editor.getBody() : false,
						$body = ( body ) ? $( body ) : false,
						$note_wrapper = ( body ) ? $body.parents( '.note-wrapper' ) : false,
						$editor,
						editor_top,
						editor_bottom,
						$window,
						window_height,
						window_scroll_top,
						window_bottom;

					// Note Template Widgets (Note Widgets that have a template selected; possibly rows/columns)
					if ( editor && $note_wrapper.length && $note_wrapper.hasClass( 'note-template-wrapper' ) ) {
						// Loop through editors
						$note_wrapper.find( '.editor' ).each( function() {
							var $this = $( this ),
								id = $this.attr( 'id' ),
								the_editor;

							// Find the TinyMCE Editor associated with this element
							the_editor = self.tinyMCE.get( id );

							// If we have an editor
							if ( the_editor ) {
								// Trigger our custom focus event
								the_editor.fire( 'note-editor-focus', data );
							}
						} );
					}
					// Standard/Default Note Widgets
					else if ( editor ) {
						// Focus the editor first
						editor.focus();

						// Move cursor to end of existing content (in the last child element)
						editor.selection.select( editor.getBody().lastChild, true );
						editor.selection.collapse( false );

						// Trigger our custom focus event
						editor.fire( 'note-editor-focus', data );
					}

					// If we have an original editor (scroll the Previewer)
					if ( editor ) {
						$editor = $( editor.getBody() );
						editor_top = $editor.offset().top;
						editor_bottom = editor_top + $editor.height();
						$window = $( window );
						window_height = $window.height();

						// Get the window scroll top and bottom
						window_scroll_top = $window.scrollTop();
						window_bottom = window_scroll_top + window_height;

						// Determine if element is not visible in the Previewer window
						if ( ! ( ( editor_bottom <= window_bottom ) && ( editor_top >= window_scroll_top ) ) ) {
							// Scroll the editor to the middle of the Previewer
							$window.scrollTop( editor_top - ( window_height / 2 ) );
						}
					}
				} );

				// Listen for the "note-widget-focus" event from the Customizer
				// TODO: Not currently in use
				self.preview.bind( 'note-widget-focus', function( data ) {
					var editor;

					// Find the correct editor
					editor = _.find( self.editors, function( editor ) {
						return editor.note.widget_data.widget.id === data.widget.id;
					} );

					// If we have an editor
					if ( editor ) {
						// Set the flag
						editor.note.focus_event = true;

						// Focus the editor
						editor.focus();

						// Move cursor to correct position
						editor.selection.setCursorLocation( editor.note.current_element, editor.note.current_offset );
					}
				} );

				// Base TinyMCE Configuration
				self.tinymce_config = {
					// TinyMCE Setup
					setup: function( editor ) {
						var contentMutationObserver;

						// Add a Note object to the editor
						editor.note = {
							widget_data: {}, // Reference to widget data
							prev_content: '', // Reference to the previous content within the editor
							focus_event: false, // Flag
							current_element: false,
							current_offset: 0,
							media: {},
							widget_id: false,
							widget_number: false,
							background_image_css: self.note.widgets.background_image_css
						};

						// Determine if we have a column TinyMCE editor and adjust selectors as necessary
						if ( editor.getParam( 'note_column_editor' ) ) {
							// Set the parent CSS selector for note_insert plugin
							editor.note.parent = '.note-col-has-editor';
						}

						// Add this editor reference to the list of editors
						self.editors.push( editor );

						// Editor initialization
						editor.on( 'init', function() {
							var $el = $( editor.getElement() ),
								$note_widget = $el.parents( '.note-widget' );

							// Store widget data on editor
							editor.note.widget_data = {
								widget: {
									number: $note_widget.find( '.widget-number' ).val(),
									id: $note_widget.find( '.widget-id' ).val()
								},
								sidebar: {
									name: $note_widget.find( '.sidebar-name' ).val(),
									id: $note_widget.find( '.sidebar-id' ).val()
								},
								// Pass default selectors to allow note-widget-update to be modified externally
								selectors: {
									widget_content: '.note-content', // Widget Content
									widget_content_data: 'note' // Widget Content Data Slug
								}
							};
							editor.note.widget_id = editor.note.widget_data.widget.id;
							editor.note.widget_number = editor.note.widget_data.widget.number;

							// Determine if we have a column TinyMCE editor and adjust selectors as necessary
							if ( editor.getParam( 'note_column_editor' ) ) {
								// Adjust the widget content selector
								editor.note.widget_data.selectors.widget_content = '.note-content-' + editor.getParam( 'note_column_editor' );
							}

							// Stop propagation to other callbacks on links to prevent Previewer refreshes
							$note_widget.on( 'click.note-widget', function( event ) {
								event.stopImmediatePropagation(); // prevent this event from bubbling up and firing other callbacks and event handlers
								event.stopPropagation(); // prevent this event from bubbling up and firing other callbacks and event handlers
							} );
						} );

						// TODO: Necessary?
						// Editor before content is set - Remove spaces from empty paragraphs.
						editor.on( 'BeforeSetContent', function( event ) {
							if ( event.content ) {
								event.content = event.content.replace( /<p>(?:&nbsp;|\u00a0|\uFEFF| )+<\/p>/gi, '<p></p>' );
							}
						} );

						// Editor focus
						editor.on( 'focus', function() {
							var content = editor.getContent(),
								data = $.extend( true, editor.note.widget_data, { widget: { content: content } } ); // Deep copy

							// Set the active flag
							self.is_widget_focused = true;

							// Send data to the Customizer
							self.preview.send( 'note-widget-focus', data );
						} );

						// Editor Note Widget focus
						editor.on( 'note-editor-focus', function() {
							// Add transition and Note edit focus CSS classes
							self.tinymce.DOM.addClass( editor.getBody(), 'mce-edit-focus-transition mce-note-edit-focus' );

							// Remove Note edit focus CSS class after 400ms
							_.delay( function() {
								self.tinymce.DOM.removeClass( editor.getBody(), 'mce-note-edit-focus' );

								// Remove the transition CSS class after another 400ms
								_.delay( function() {
									self.tinymce.DOM.removeClass( editor.getBody(), 'mce-edit-focus-transition' );
								}, self.transition_duration );
							}, self.transition_duration ); // CSS transition is 400ms
						} );

						// Editor blur
						editor.on( 'blur', function() {
							var content = editor.getContent(),
								data = $.extend( true, editor.note.widget_data, { widget: { content: content } } ); // Deep copy

							// Set the active flag
							self.is_widget_focused = false;

							// Send data to the Customizer
							self.preview.send( 'note-widget-blur', data );
						} );

						/*
						 * In WordPress 4.7, logic was added to determine if links in the Previewer
						 * are considered previewable. Since we're over-riding the isLinkPreviewable() function
						 * below, we need to make sure that the "customize-unpreviewable" CSS class is removed
						 * from link elements within Note Widgets as this CSS class will affect Note Widgets
						 * in negative ways. The "customize-unpreviewable" CSS class is added to any link that
						 * WordPress considers to be unpreviewable.
						 */
						if ( parseFloat( self.note_tinymce.wp_version ) >= 4.7 ) {
							// Create a new MutationObserver
							contentMutationObserver = new MutationObserver( function( mutations ) {
								// Loop through mutations
								_.each( mutations, function( mutation ) {
									// Switch based on type of mutation
									switch ( mutation.type ) {
										// Attributes (we're only monitoring the class attribute via the attributeFilter config property)
										case 'attributes':
											var $el = $( mutation.target );

											// If we don't have an old value and the customize-unpreviewable CSS class exists
											if ( ! mutation.oldValue || $el.hasClass( 'customize-unpreviewable' ) ) {
												// Remove the customize-unpreviewable CSS class
												$el.removeClass( 'customize-unpreviewable' );
											}
										break;
									}
								} );
							} );

							// Observe
							contentMutationObserver.observe( editor.getElement(), {
								childList: true, // TODO: Is this necessary?
								attributes: true,
								subtree: true,
								attributeOldValue: true,
								attributeFilter: [
									'class'
								]
							} );
						}

						// A change within the editor content has occurred
						// TODO: Create a function that can send updated data to the Customizer based on parameters that sits outside of this logic so that other developers and plugins can hook into Note easier (adjust widget_content to element in Customizer logic for this functionality; optimized functionality/logic)
						editor.on( 'keyup change NodeChange SetAttrib', _.debounce( function() {
							var content = editor.getContent(),
								data = {};

							// Content within the editor has changed or this is an initial Previewer load
							if ( ( ! editor.note.hasOwnProperty( 'prevent_widget_update' ) || ! editor.note.prevent_widget_update ) && ( editor.note.prev_content === '' || editor.note.prev_content !== content ) ) {
								// Deep copy
								data = $.extend( true, editor.note.widget_data, { widget: { content: content } } );

								// Send data to the Customizer
								self.preview.send( 'note-widget-update', data );

								// Update the previous content reference
								editor.note.prev_content = content;
							}
						}, 300 ) ); // 300ms debounce delay


						/*
						 * Determine if we have any TinyMCE modal commands that should set the active
						 * or inactive modal flags in the Customizer.
						 */

						// Activate
						if ( self.modal_commands && self.modal_commands.hasOwnProperty( 'activate' ) ) {
							// Setup active modal commands
							self.setupModalCommands( 'activate', self.modal_commands.activate, {
								editor: editor
							} );
						}

						// Deactivate
						if ( self.modal_commands && self.modal_commands.hasOwnProperty( 'deactivate' ) ) {
							// Setup active modal commands
							self.setupModalCommands( 'deactivate', self.modal_commands.deactivate, {
								editor: editor
							} );
						}
					}
				};

				// Merge TinyMCE config data with default configuration data
				self.note.tinymce[self.default_note_template_config_type] = _.extend( self.note.tinymce[self.default_note_template_config_type], self.tinymce_config );


				// Loop through widgets/settings
				if ( ! _.isEmpty( self.widget_settings ) ) {
					// Loop through widget settings
					_.each( self.widget_settings, function( settings ) {
						var template = ( ! _.isEmpty( self.widget_templates ) && self.widget_templates.hasOwnProperty( settings.template ) ) ? self.widget_templates[settings.template] : false,
							template_config = ( template && template.hasOwnProperty( 'config' ) ) ? template.config : {},
							template_config_type = ( template_config && template_config.hasOwnProperty( 'type' ) ) ? template_config.type : self.default_note_template_config_type,
							// TODO: Is there a more efficient way to find the correct widget element?
							$widget = $( _.find( self.$note_widgets, function( widget ) {
								return $( widget ).find( '.widget-id' ).val() === settings.widget_id;
							} ) ),
							widget_id = ( $widget ) ? $widget.attr( 'id' ) : false,
							$editors = ( $widget.length ) ? $widget.find( '.editor-content' ) : false;

						// If we have a widget and a template
						if ( $widget.length && template ) {
							// If we have editor elements and template columns
							if ( $editors.length && template_config.hasOwnProperty( 'columns' ) ) {
								var template_columns = template_config.columns;

								// Loop through editors
								$editors.each( function() {
									var $this = $( this ),
										$parent = $this.parent(),
										note_column = $parent.data( 'note-column' ),
										note_editor_id = $parent.data( 'note-editor-id' );

									// Reset the template config data
									template_config = ( template && template.hasOwnProperty( 'config' ) ) ? template.config : {};

									// Adjust the column configuration accordingly
									if ( note_column && template_columns.hasOwnProperty( note_column ) ) {
										template_config = _.defaults( _.clone( template_columns[note_column] ), _.clone( template_config ) );
									}

									// Determine the more specific template type (if set)
									template_config_type = template_config.type || self.default_note_template_config_type;

									// Initialize TinyMCE
									self.initTinyMCE( {
										// Adjust the selector to match this particular widget and editor
										selector: '#' + widget_id + ' .editor-' + note_editor_id,
										note_column_editor: note_editor_id
									}, template_config, template_config_type );
								} );
							}
							// Otherwise just setup one editor
							else {
								// Bail if the editor content element doesn't exist
								if ( ! $( '#' + widget_id + ' .editor-content' ).length ) {
									return;
								}

								self.initTinyMCE( {
									// Adjust the selector to match this particular widget and editor
									selector: '#' + widget_id + ' .editor-content'
								}, template_config, template_config_type );
							}
						}
					} );


					/*
					 * Now we have to initialize all default Note Widgets just one time (this all add TinyMCE to each widget)
					 */

					// Add the selector to array
					self.editor_selectors.push( self.note.tinymce[self.default_note_template_config_type].selector );

					// Add this editor config to array (will be populated with self.note.tinymce[self.default_note_template_config_type] data after TinyMCE init)
					self.editor_config.push( self.note.tinymce[self.default_note_template_config_type] );

					// Init TinyMCE
					self.tinymce.init( self.note.tinymce[self.default_note_template_config_type] );
				}

				/*
				 * Determine if we have any modal commands that should set the active
				 * or inactive modal flags in the Customizer.
				 */

				// Activate
				if ( self.modal_commands && self.modal_commands.hasOwnProperty( 'activate' ) ) {
					// Setup active modal commands
					self.setupModalCommands( 'activate', self.modal_commands.activate, {
						document: self.$document,
						media: wp.media
					} );
				}

				// Deactivate
				if ( self.modal_commands && self.modal_commands.hasOwnProperty( 'deactivate' ) ) {
					// Setup active modal commands
					self.setupModalCommands( 'deactivate', self.modal_commands.deactivate, {
						document: self.$document,
						media: wp.media
					} );
				}


				/*
				 * Note Sidebars
				 */

				// Note Sidebars exist
				if ( note.hasOwnProperty( 'sidebars' ) && note.sidebars.hasOwnProperty( 'args' ) && note.sidebars.args ) {
					// Send the Note Sidebar arguments to the Customizer (specific to the page being displayed)
					self.preview.send( 'note-sidebar-args', note.sidebars.args );

					/*
					 * Note UI Buttons
					 */

					// Note Edit Sidebar Button Mouseenter
					self.$document.on( 'mouseenter.note', '.note-edit-sidebar-button', function( event ) {
						var $this = $( this );

						// Stop the timer to remove "hover" CSS class
						clearTimeout( $this.data( 'note-hover-timer' ) );

						// Add the "hover" CSS classes
						$this.parent().addClass( 'hover' );

						$this.parents( '.note-sidebar' ).addClass( 'note-edit-border' );
					} );

					// Note Edit Sidebar Button Mouseleave
					self.$document.on( 'mouseleave.note', '.note-edit-sidebar-button', function( event ) {
						var $this = $( this );

						// Remove the "hover" CSS classes after 400ms
						$this.data( 'note-hover-timer', setTimeout( function() {
							$this.parent().removeClass( 'hover' );

							$this.parents( '.note-sidebar' ).removeClass( 'note-edit-border' );
						}, self.transition_duration ) );
					} );

					// Note Edit Sidebar Button Click
					self.$document.on( 'touch click', '.note-edit-sidebar-button', function( event ) {
						var $this = $( this ),
							$sidebar = $this.parents( '.note-sidebar' ),
							sidebar_id = $sidebar.attr( 'data-note-sidebar-id' );

						// Send the 'note-edit-sidebar' data to the Customizer
						self.preview.send( 'note-edit-sidebar', {
							sidebar_id: ( $sidebar.attr( 'data-note-sidebar' ) === 'true' ) ? note.sidebars.args[sidebar_id].customizer.section.sidebarId : sidebar_id
						} );
					} );

					// Note Secondary Buttons Mouseenter
					self.$document.on( 'mouseenter.note', '.note-secondary-button-wrap', function( event ) {
						var $this = $( this ),
							$edit_button = $this.parent().find( '.note-edit-sidebar-button' );

						// Stop the timer to remove "hover" CSS class
						clearTimeout( $edit_button.data( 'note-hover-timer' ) );
					} );

					// Note Secondary Buttons Mouseleave
					self.$document.on( 'mouseleave.note', '.note-secondary-button-wrap', function( event ) {
						var $this = $( this ),
							$edit_button = $this.parent().find( '.note-edit-sidebar-button' );

						// Remove the "hover" CSS classes after 400ms
						$edit_button.data( 'note-hover-timer', setTimeout( function() {
							$edit_button.parent().removeClass( 'hover' );

							$edit_button.parents( '.note-sidebar' ).removeClass( 'note-edit-border' );
						}, self.transition_duration ) );
					} );

					// Note Add Widget Button
					self.$document.on( 'touch click', '.note-add-widget-button', function( event ) {
						var $this = $( this ),
							$sidebar = $this.parents( '.note-sidebar' ),
							sidebar_id = $sidebar.attr( 'data-note-sidebar-id' );

						// Send the 'note-add-widget' data to the Customizer
						self.preview.send( 'note-add-widget', {
							sidebar_id: ( $sidebar.attr( 'data-note-sidebar' ) === 'true' ) ? note.sidebars.args[sidebar_id].customizer.section.sidebarId : sidebar_id
						} );
					} );

					// Note Add Note Widget Button
					self.$document.on( 'touch click', '.note-add-note-widget-button', function( event ) {
						var $this = $( this ),
							$sidebar = $this.parents( '.note-sidebar' ),
							sidebar_id = $sidebar.attr( 'data-note-sidebar-id' );

						// Send the 'note-add-note-widget' data to the Customizer
						self.preview.send( 'note-add-note-widget', {
							sidebar_id: ( $sidebar.attr( 'data-note-sidebar' ) === 'true' ) ? note.sidebars.args[sidebar_id].customizer.section.sidebarId : sidebar_id,
							widget_id: note.widget.id
						} );
					} );

					// Note Remove Note Sidebar Button
					self.$document.on( 'touch click', '.note-remove-note-sidebar-button', function( event ) {
						var $this = $( this ),
							$sidebar = $this.parents( '.note-sidebar' ),
							post_id = $sidebar.attr( 'data-post-id' ),
							sidebar_id = $sidebar.attr( 'data-note-sidebar-id' );

						// Render the modal
						api.NotePreview.views.modals.unregister_sidebar.modal.render( {
							command: 'note-unregister-sidebar',
							data: {
								post_id: post_id,
								note_sidebar_id: sidebar_id
							}
						} );
					} );
				}
			} );
		},
		/**
		 * Initialize a TinyMCE instance.
		 */
		initTinyMCE: function( editor_config_defaults, template_config, config_type ) {
			// Editor configuration
			var self = this,
				editor_config = _.defaults( editor_config_defaults, self.tinymce_config ),
				editor_tinymce_config;

			// Add the selector to array
			self.editor_selectors.push( editor_config.selector );

			// Add this editor config to array (will be populated with self.note.tinymce[self.default_note_template_config_type] data after TinyMCE init)
			self.editor_config.push( editor_config );

			// Setup TinyMCE config based on type
			if ( self.note.tinymce.hasOwnProperty( config_type ) && !_.isEmpty( self.note.tinymce[config_type] ) ) {
				editor_tinymce_config = self.note.tinymce[config_type];

				// TODO: Allow plugins?, blocks, and toolbar items to be spliced in at certain areas in the data

				// If there are any plugins that should added to this editor from the template config data
				if ( template_config.hasOwnProperty( 'plugins' ) && editor_tinymce_config.hasOwnProperty( 'plugins' ) ) {
					// Loop through plugins
					_.each( template_config.plugins, function( plugin ) {
						// Split the plugins into an array
						if ( ! editor_tinymce_config.hasOwnProperty( 'plugins_arr' ) ) {
							editor_tinymce_config.plugins_arr = editor_tinymce_config.plugins.split( ' ' );
						}

						// If we have plugins in the plugins array and this plugin doesn't already exist
						if ( editor_tinymce_config.plugins_arr.length && editor_tinymce_config.plugins_arr.indexOf( plugin ) === -1 ) {
							// Add this plugin to the array of existing plugins
							editor_tinymce_config.plugins_arr.push( plugin );

							// Append this plugin to the list of existing plugins
							editor_tinymce_config.plugins += ' ' + plugin;
						}
					} );
				}

				// If there are any blocks that should added to this editor from the template config data
				if ( template_config.hasOwnProperty( 'blocks' ) && editor_tinymce_config.hasOwnProperty( 'blocks' ) ) {
					// Loop through plugins
					_.each( template_config.blocks, function( block ) {
						// If this block doesn't already exist
						if ( editor_tinymce_config.blocks.indexOf( block ) === -1 ) {
							// Add this block to the list of existing blocks
							editor_tinymce_config.blocks.push( block );
						}
					} );
				}

				// If there are any toolbar items that should added to this editor from the template config data
				if ( template_config.hasOwnProperty( 'toolbar' ) && editor_tinymce_config.hasOwnProperty( 'toolbar' ) ) {
					// Loop through plugins
					_.each( template_config.toolbar, function( item ) {
						// If this toolbar item doesn't already exist
						if ( editor_tinymce_config.toolbars.indexOf( item ) === -1 ) {
							// Add this toolbar item to the list of existing toolbars
							editor_tinymce_config.toolbars.push( item );
						}
					} );
				}
			}
			// Otherwise fallback to the default setup
			else {
				editor_tinymce_config = self.note.tinymce[self.default_note_template_config_type];
			}

			// Init TinyMCE (using _.defaults() instead of _.extend() to make sure we're not altering default Note data)
			self.tinymce.init( _.defaults( editor_config, editor_tinymce_config ) );
		},
		/**
		 * Setup modal commands.
		 */
		setupModalCommands: function( type, commands, targets ) {
			var self = this;

			// Loop through commands
			for ( var command_type in commands ) {
				// hasOwnProperty
				if ( commands.hasOwnProperty( command_type ) ) {
					var target; // Reference to the object being targeted

					// Switch based on the type of command
					switch ( command_type ) {
						// TinyMCE
						case 'tinymce':
							// Reference to the editor
							target = targets.editor;
						break;

						// jQuery Document
						case 'document':
							// Reference to the document
							target = targets.document;
						break;

						// wp.media Events
						case 'wp.media.events':
							// Reference to wp.media.events
							target = targets.media && targets.media.events;
						break;

						// wp.media Frame
						case 'wp.media.frame':
							// Reference to wp.media.frame
							target = wp.media.frame;
						break;
					}

					// Determine if we actually have a target and any activate commands
					if ( target && ! _.isEmpty( commands[command_type] ) ) {
						// Arrays (constructor is the fastest method to check)
						if ( commands[command_type].constructor ===  Array ) {
							// Loop through commands
							for ( var i = 0; i < commands[command_type].length; i++ ) {
								// Setup the event listener
								self.addEventListenerToObject( type, target, targets.editor, targets.media, {
									id: _.uniqueId( 'note_modal_command_' ), // Unique ID for this command
									command: commands[command_type][i],
									sub_command: false,
									command_type: command_type
								} );
							}
						}
						// Objects
						else {
							// Loop through commands
							for ( var command in commands[command_type] ) {
								// hasOwnProperty
								if ( commands[command_type].hasOwnProperty( command ) ) {
									var is_key_numerical = ! isNaN( parseInt( command, 10 ) ),
										// If we have a numerical key, the command is the property value
										the_command = ( is_key_numerical ) ? commands[command_type][command] : command,
										// If we don't have a numerical key, the sub-command is the property value (if not empty)
										sub_command = ( ! is_key_numerical && commands[command_type][command] ) ? commands[command_type][command] : false;

									// Setup the event listener
									self.addEventListenerToObject( type, target, targets.editor, targets.media, {
										id: _.uniqueId( 'note_modal_command_' ), // Unique ID for this command
										command: the_command,
										sub_command: sub_command,
										command_type: command_type
									} );
								}
							}
						}
					}
				}
			}
		},
		/**
		 * This function adds an event listener to an object. It also stores a reference to the
		 * event listener.
		 */
		addEventListenerToObject: function( type, target, editor, media, command ) {
			var self = this,
				listener;

			// Setup the listener function
			listener = function( event ) {
				var event_sub_command = false,
					content = editor && editor.getContent() || '', // Get the editor content
					data = $.extend( true, editor && editor.note && editor.note.widget_data || {}, { widget: { content: content } } ), // Deep copy
					obj_keys = [], // Reference to sub-command object keys
					listener_obj_keys = [], // Reference to listener sub-command object keys
					sub_target,
					command_listeners = self.modal_command_listeners[type],
					modal_command_listener;

				// If we have a sub-command that should be listened to (string)
				if ( command.sub_command && typeof command.sub_command === 'string' ) {
					// Switch based on command type
					switch ( command.command_type ) {
						// TinyMCE (event.command contains the sub-command)
						case 'tinymce':
							event_sub_command = event.command; // Get the sub-command
						break;
					}

					// If we have an event sub-command, check it first
					if ( event_sub_command && event_sub_command === command.sub_command ) {
						// Send the modal flag command
						self.sendModalFlagCommand( type, data );
					}
				}
				// Otherwise if we have a sub-command that should be listened to (object)
				// Nested sub-commands are considered to be only one level deep
				else if ( command.sub_command && typeof command.sub_command === 'object' ) {
					// Get the object keys
					obj_keys = Object.keys( command.sub_command );

					// First check to see if this command has already been attached and called
					if ( command_listeners.length ) {
						for ( var i = 0; i < command_listeners.length; i++ ) {
							// First check the command name and the sub-command
							if ( command_listeners[i].command.command === command.command && typeof command_listeners[i].command.sub_command === 'object'  ) {
								// Get the listener object keys
								listener_obj_keys = Object.keys( command_listeners[i].command.sub_command );

								// Bail if the sub-command matches and that it has been called
								if ( listener_obj_keys[0] == obj_keys[0] && command_listeners[i].command.sub_command[obj_keys[0]] === command.sub_command[obj_keys[0]] && command_listeners[i].callback_count > 0 ) {
									return;
								}
							}
						}
					}

					// Switch based on object keys
					switch ( obj_keys[0] ) {
						// wp.media Events
						case 'wp.media.events':
							// Reference to wp.media.events
							sub_target = wp.media.events;
						break;

						// wp.media Frame
						case 'wp.media.frame':
							// Reference to wp.media.frame
							sub_target = wp.media.frame;
						break;
					}

					// Special case for the editor frame
					// TODO: How might we handle other special cases?
					if ( command.command === 'editor:frame-create' && obj_keys[0] === 'event.frame' && command.sub_command[obj_keys[0]] === 'close' ) {
						// Add the event listener to the sub target
						event.frame.on( command.sub_command[obj_keys[0]], function() {
							// Send the modal flag command
							self.sendModalFlagCommand( type, data );
						} );
					}
					// Regular sub-command
					else {
						// Add the event listener to the sub target
						sub_target.on( command.sub_command[obj_keys[0]], function() {
							// Send the modal flag command
							self.sendModalFlagCommand( type, data );
						} );
					}
				}
				// Regular command
				else {
					// Send the modal flag command
					self.sendModalFlagCommand( type, data );
				}

				// Increase callback count for this listener
				if ( self.modal_command_listeners[type].length ) {
					modal_command_listener = _.find( self.modal_command_listeners[type], function( listener ) {
						return listener.command.id === command.id;
					} );

					// If we have a match, increase the count
					if ( modal_command_listener ) {
						modal_command_listener.callback_count++;
					}
				}
			};

			// Add the listener to the reference array
			self.modal_command_listeners[type].push( {
				target: target,
				command: command,
				listener: listener,
				callback_count: 0 // Number of times the listener was called
			} );

			// Add listener to target/command
			target.on( command.command, listener );
		},
		// Send active/inactive command to Customizer
		sendModalFlagCommand: function( type, data ) {
			var self = this;

			// Switch based on the type of command
			switch ( type ) {
				// Activate
				case 'activate':
					// Set the active flag
					self.is_widget_modal_active = true;

					// Send data to Customizer
					self.preview.send( 'note-widget-modal-active', data );
				break;
				// Deactivate
				case 'deactivate':
					// Reset the active flag
					self.is_widget_modal_active = false;

					// Send data to Customizer
					self.preview.send( 'note-widget-modal-inactive', true );
				break;
			}
		},
		// Note WP/Backbone Views
		Views: {
			// Modal
			Modal: wp.Backbone.View.extend( {
				el: '#note-modal',
				overlay_el: '#note-modal-overlay',
				content_el: '#note-modal-content',
				// Initialize
				initialize: function() {
					// Bind "this" to all functions
					_.bindAll(
						this,
						'render',
						'open',
						'close'
					);
				},
				// Render
				render: function( data ) {
					// Setup submit data across subviews first
					if ( this.hasData( data, 'command' ) && this.hasData( data, 'data' ) ) {
						this.setupSubmitData( data.command, data.data );
					}

					// Verify that we've passed localStorage checks before rendering
					if ( this.hasData( data, 'localStorage' ) && this.checklocalStorageData( data.localStorage.key, data.localStorage.value ) ) {
						// "Close" the modal if localStorage data is set
						this.close( {}, true );

						return this;
					}

					// Call (apply) the default wp.Backbone.View render function
					wp.Backbone.View.prototype.render.apply( this, arguments );

					// "Open" the modal
					this.open();

					// Setup localStorage data across subviews
					if ( this.hasData( data, 'localStorage' ) ) {
						this.setuplocalStorageData( data.localStorage.key, data.localStorage.value );
					}

					return this;
				},
				// This function runs on the opening of the modal overlay
				open: function() {
					var subviews = this.views.all();

					// Show the modal element
					this.$el.show();

					// Loop through subviews
					_.each( subviews, function ( view ) {
						// If the sub-vew has an open function
						if ( view.hasOwnProperty( 'open' ) && _.isFunction( view.open ) ) {
							// Open the sub-view
							view.open();
						}
					} );

					// Trigger an event on the document
					api.NotePreview.$document.trigger( 'note-modal-open', this );

					// Allow chaining
					return this;
				},
				// This function runs on the closing of the modal overlay
				close: function( event, submit ) {
					var subviews = this.views.all();

					// Trigger an event on the document
					api.NotePreview.$document.trigger( 'note-modal-close', this );

					// Hide the modal element
					this.$el.hide();

					// Reset the rendered flag
					this.views.rendered = false;

					// Loop through subviews
					_.each( subviews, function ( view ) {
						// If the sub-vew has a close function
						if ( view.hasOwnProperty( 'close' ) && _.isFunction( view.close ) ) {
							// Close the sub-view
							view.close( event, submit );

							// Reset the rendered flag
							view.views.rendered = false;
						}
					} );

					// Allow chaining
					return this;
				},
				// This function passes render data to subviews
				setupSubmitData: function( command, data ) {
					var subviews = this.views.all();

					// Loop through subviews
					_.each( subviews, function ( view ) {
						// Add submit command
						view.options.submit_command = command;

						// Add submit data
						view.options.submit_data = data;
					} );
				},
				// This function passes localStorage data to subviews
				setuplocalStorageData: function( key, value ) {
					var subviews = this.views.all();

					// Loop through subviews
					_.each( subviews, function ( view ) {
						// Create the localStorage option
						view.options.localStorage = view.options.localStorage || {};

						// Add submit command
						view.options.localStorage.key = key;

						// Add submit data
						view.options.localStorage.value = value;
					} );
				},
				// This function checks to see if data exists
				hasData: function( data, key ) {
					return data.hasOwnProperty( key );
				},
				// This function checks localStorage data to verify
				checklocalStorageData: function( key, value ) {
					var localStorageData = ( localStorage['note'] !== undefined ) ? JSON.parse( localStorage['note'] ) : {};

					// Determine if we have a key and the value matches
					return localStorageData['modals'] && localStorageData['modals'][key] && localStorageData['modals'][key] === value;
				}
			} ),
			// Modal Content
			ModalContent: wp.Backbone.View.extend( {
				template: wp.template( 'note-modal-content' ),
				// Events
				events: {
					'click.note .note-modal-close': 'closeModal',
					'click.note .note-modal-cancel': 'closeModal',
					'click.note .note-modal-submit': function( event ) {
						// Prevent default
						event.preventDefault();

						// Close the modal
						this.closeModal( event, true );
					}
				},
				// Initialize
				initialize: function() {
					// Bind "this" to all functions
					_.bindAll(
						this,
						'render',
						'open',
						'close',
						'closeModal',
						'sendSubmitData',
						'setlocalStorageData'
					);
				},
				// Render
				render: function( data ) {
					// Call (apply) the default wp.Backbone.View render function
					wp.Backbone.View.prototype.render.apply( this, arguments );
				},
				// This function runs on the opening of the modal TODO
				open: function() {
					// Show the content element (parent element)
					this.$el.parent().show();

					// TODO: Reset HTML here if necessary
					// TODO: Open logic here if necessary
				},
				// This function runs on the closing of the modal TODO
				close: function( event, submit ) {
					// Hide the content element (parent element)
					this.$el.parent().hide();

					// If the modal was "submit"ed
					if ( submit ) {
						// Set the localStorage data
						if ( this.hasData( this.options, 'localStorage' ) ) {
							this.setlocalStorageData();
						}

						// Send the submission data
						if ( this.hasData( this.options, 'submit_command' ) && this.hasData( this.options, 'submit_data' ) ) {
							this.sendSubmitData( event );
						}
					}

					// Clear the html
					this.$el.html( '' );

					// TODO: Close logic here if necessary
				},
				// This function closes the modal
				closeModal: function( event, submit ) {
					// Prevent default
					if ( event.preventDefault && _.isFunction( event.preventDefault ) )  {
						event.preventDefault();
					}

					// Since this is a sub-view, call the parent view close() method, which calls all sub-view close() methods if they exist
					this.views.parent.close( event, submit );

					// Allow chaining
					return this;
				},
				// This function sends data to the Customizer
				sendSubmitData: function( event ) {
					var self = this,
						$inputs = this.$( 'input, textarea', this.$( '.note-modal-content' ) );

					// Prevent default
					if ( event.hasOwnProperty( 'preventDefault' ) && _.isFuncton( event.preventDefault ) )  {
						event.preventDefault();
					}

					// Determine if there were any input elements, merge their data
					if ( $inputs.length ) {
						// Loop through inputs
						$inputs.each( function() {
							var $this = $( this ),
								type = $this.attr( 'type' );

							// Checkboxes and radio buttons
							if ( type === 'checkbox' || type === 'radio' ) {
								self.options.submit_data[$this.attr( 'name' )] = $this.prop( 'checked' );
							}
							// All other inputs
							else {
								self.options.submit_data[$this.attr( 'name' )] = $this.val();
							}
						} );
					}

					// Send the command data to the Customizer
					api.NotePreview.preview.send( this.options.submit_command, this.options.submit_data );

					// Allow chaining
					return this;
				},
				// This function checks to see if data exists
				hasData: function( data, key ) {
					return data.hasOwnProperty( key );
				},
				// This function sets localStorage data
				setlocalStorageData: function() {
					var self = this,
						$inputs = this.$( 'input, textarea', this.$( '.note-modal-content' ) ),
						localStorageData = ( localStorage['note'] !== undefined ) ? JSON.parse( localStorage['note'] ) : {};

					// Add the modals key
					localStorageData['modals'] = localStorageData['modals'] || {};

					// Determine if there were any input elements, merge their data
					if ( $inputs.length ) {
						// Loop through inputs
						$inputs.each( function() {
							var $this = $( this ),
								type = $this.attr( 'type' );

							// Checkboxes and radio buttons
							if ( ( type === 'checkbox' || type === 'radio' ) && $this.attr( 'name' ) === self.options.localStorage.key ) {
								localStorageData['modals'][$this.attr( 'name' )] = $this.prop( 'checked' );
							}
							// All other inputs
							else if ( $this.attr( 'name' ) === self.options.localStorage.key ) {
								localStorageData['modals'][$this.attr( 'name' )] = $this.val();
							}
						} );

						// store the localStorage data
						localStorage['note'] = JSON.stringify( localStorageData );
					}

					// Allow chaining
					return this;
				}
			} ),
			// Modal Overlay
			ModalOverlay: wp.Backbone.View.extend( {
				//template: wp.template( 'note-modal-overlay' ),
				initialize: function() {
					// Bind "this" to all functions
					_.bindAll(
						this,
						'render',
						'open',
						'close'
					);
				},
				// Render
				render: function( data ) {
					// Call (apply) the default wp.Backbone.View render function
					wp.Backbone.View.prototype.render.apply( this, arguments );

					// "Open" the overlay
					this.open();
				},
				// This function runs on the opening of the modal overlay
				open: function() {
					// Add the modal-open CSS class to the <body> element
					api.NotePreview.$body.addClass( 'modal-open' );

					// Show the overlay (parent element)
					this.$el.parent().show();

					// Allow chaining
					return this;
				},
				// This function runs on the closing of the modal overlay
				close: function() {
					// Remove the modal-open CSS class to the <body> element
					api.NotePreview.$body.removeClass( 'modal-open' );

					// Hide the overlay (parent element)
					this.$el.parent().hide();

					// Allow chaining
					return this;
				}
			} )
		},
		// Reference to all views created be NotePreviewer
		views: {
			// Modal views
			modals: {
				// Register Sidebar
				register_sidebar: {},
				// Unregister (Remove) Sidebar
				unregister_sidebar: {}
			}
		},
		// Note WP/Backbone Models TODO
		Models: {},
		// Reference to all models created be NotePreviewer
		models: {
			// Modal models
			modals: {
				// Register Sidebar
				register_sidebar: {},
				// Unregister (Remove) Sidebar
				unregister_sidebar: {}
			}
		}
		// TODO: Function to remove event listeners
	};

	/**
	 * Capture the instance of the Preview since it is private
	 */
	OldPreview = api.Preview;
	api.Preview = OldPreview.extend( {
		initialize: function( params, options ) {
			/**
			 * Modal Windows - We're adding this event handler here in order to ensure it is triggered
			 * before the Customize Preview click event is bound as jQuery calls handlers in order
			 * of registration. The Customize Preview event is bound once the api.Preview is constructed.
			 */

			// Stop propagation to other callbacks on modal links to prevent Previewer refreshes
			$( document.body ).on( 'click.note', '.media-modal a, .wp-link-wrap a, #note-modal a', function( event ) {
				event.stopImmediatePropagation(); // prevent this event from bubbling up and firing other callbacks and event handlers
			} );

			api.NotePreview.preview = this;
			OldPreview.prototype.initialize.call( this, params, options );
		}
	} );

	/**
	 * Grab the isLinkPreviewable() function since it is private
	 */
	isLinkPreviewable = api.isLinkPreviewable;

	// If the isLinkPreviewable() function exists
	if ( isLinkPreviewable ) {
		// Add our own isLinkPreviewable() function
		api.isLinkPreviewable = function( element, options ) {
			var $this = $( element ),
				$note_widget = $this.parents( '.note-widget' );

			// If this is a Note Widget, don't allow previewable links
			if ( $note_widget.length ) {
				// TODO: Do we need to remove the existing Customizer query arguments here?

				return false;
			}

			// Otherwise let WordPress determine if this element is previewable
			return isLinkPreviewable.call( this, element, options );
		};
	}

	/**
	 * Document Ready
	 */
	$( function() {
		var note = window.note,
			$note_sidebar_placeholder = $( '.note-sidebar-placeholder' ),
			$note_sidebar_placeholder_register = $( '.note-sidebar-placeholder-register' ),
			note_modal_models = api.NotePreview.models.modals,
			note_modal_views = api.NotePreview.views.modals;

		if ( ! note ) {
			return;
		}

		// Extend our Note Preview parameters with Note data
		$.extend( api.NotePreview, note );

		// Initialize our custom Preview
		api.NotePreview.init();


		/*
		 * Note Sidebars
		 */

		/*
		 * Note Sidebars - WP/Backbone Models & Views for registering a sidebar
		 */

		// Modal Content Model
		note_modal_models.register_sidebar.modal_content = new Backbone.Model( {
			title: note.modals.register_sidebar.title,
			content: note.modals.register_sidebar.content,
			submit_label: note.modals.register_sidebar.submit_label
		} );

		// Modal Content View
		note_modal_views.register_sidebar.modal_content = new api.NotePreview.Views.ModalContent( {
			model: note_modal_models.register_sidebar.modal_content, // Model
			title: note_modal_models.register_sidebar.modal_content.get( 'title' ), // Title
			content: note_modal_models.register_sidebar.modal_content.get( 'content' ), // Content
			submit_label: note_modal_models.register_sidebar.modal_content.get( 'submit_label' ) // Submit Button Label
		} );

		// Modal Overlay View
		note_modal_views.register_sidebar.modal_overlay = new api.NotePreview.Views.ModalOverlay();

		// Modal View
		note_modal_views.register_sidebar.modal = new api.NotePreview.Views.Modal();

		// Modal Subviews
		note_modal_views.register_sidebar.modal.views.set(
			note_modal_views.register_sidebar.modal.overlay_el,
			note_modal_views.register_sidebar.modal_overlay,
			{ silent: true } // No DOM modifications
		); // Attach modal overlay view
		note_modal_views.register_sidebar.modal.views.set(
			note_modal_views.register_sidebar.modal.content_el,
			note_modal_views.register_sidebar.modal_content,
			{ silent: true } // No DOM modifications
		); // Attach modal content view


		/*
		 * Note Sidebars - WP/Backbone Models & Views for unregistering (removing) a sidebar
		 */

		// Modal Content Model
		note_modal_models.unregister_sidebar.modal_content = new Backbone.Model( {
			title: note.modals.unregister_sidebar.title,
			content: note.modals.unregister_sidebar.content,
			submit_label: note.modals.unregister_sidebar.submit_label
		} );

		// Modal Content View
		note_modal_views.unregister_sidebar.modal_content = new api.NotePreview.Views.ModalContent( {
			model: note_modal_models.unregister_sidebar.modal_content, // Model
			title: note_modal_models.unregister_sidebar.modal_content.get( 'title' ), // Title
			content: note_modal_models.unregister_sidebar.modal_content.get( 'content' ), // Content
			submit_label: note_modal_models.unregister_sidebar.modal_content.get( 'submit_label' ) // Submit Button Label
		} );

		// Modal Overlay View
		note_modal_views.unregister_sidebar.modal_overlay = new api.NotePreview.Views.ModalOverlay();

		// Modal View
		note_modal_views.unregister_sidebar.modal = new api.NotePreview.Views.Modal();

		// Modal Subviews
		note_modal_views.unregister_sidebar.modal.views.set(
			note_modal_views.unregister_sidebar.modal.overlay_el,
			note_modal_views.unregister_sidebar.modal_overlay,
			{ silent: true } // No DOM modifications
		); // Attach modal overlay view
		note_modal_views.unregister_sidebar.modal.views.set(
			note_modal_views.unregister_sidebar.modal.content_el,
			note_modal_views.unregister_sidebar.modal_content,
			{ silent: true } // No DOM modifications
		); // Attach modal content view


		/*
		 * Note Sidebars - Placeholders
		 */

		// Mouseover
		$note_sidebar_placeholder.on( 'mouseover', function( event ) {
			var $this = $( this );

			// Stop the timer to remove "hover" CSS class
			clearTimeout( $this.data( 'note-hover-timer' ) );

			// Stop the timer to remove "pulse" CSS class
			clearTimeout( $this.data( 'note-pulse-timer' ) );

			// Add the "hover pulse" CSS classes
			$this.addClass( 'hover pulse' );
		} );

		// Mousemove
		$note_sidebar_placeholder.on( 'mousemove', function( event ) {
			var $this = $( this ),
				$edit = $this.find( '.note-sidebar-register' ),
				el_left = Math.ceil( event.pageX - $this.offset().left );


			// Stop the timer to remove "hover" CSS class
			clearTimeout( $this.data( 'note-hover-timer' ) );

			// Stop the timer to remove "pulse" CSS class
			clearTimeout( $this.data( 'note-pulse-timer' ) );

			// Stop the timer to remove "mousemove" CSS class
			clearTimeout( $this.data( 'note-mousemove-timer' ) );

			// Add the "hover pulse" CSS classes
			$this.addClass( 'mousemove' );

			// New left position is outside of placeholder boundary (right; width)
			if ( el_left > $this.width() ) {
				el_left = $this.width();
			}
			// New left position is outside of placeholder boundary (left; zero)
			else if ( el_left < 0 ) {
				el_left = 0;
			}

			// Adjust the left position of the edit button
			$edit.css( 'left', el_left );

			// Remove the "mousemove" CSS class after 400ms
			$this.data( 'note-mousemove-timer', setTimeout( function() {
				$this.removeClass( 'mousemove' );
			}, api.NotePreview.transition_duration ) );
		} );

		// Mouseout
		$note_sidebar_placeholder.on( 'mouseout', function( event ) {
			var $this = $( this ),
				$edit = $this.find( '.note-sidebar-register' );

			// Remove the "pulse" and "mousemove" CSS classes
			$this.removeClass( 'mousemove' );

			// Remove the "pulse" CSS class after 200ms
			$this.data( 'note-pulse-timer', setTimeout( function() {
				$this.removeClass( 'pulse' );
			}, ( api.NotePreview.transition_duration - 200 ) ) );

			// Remove the "hover" CSS class after 400ms
			$this.data( 'note-hover-timer', setTimeout( function() {
				$this.removeClass( 'hover' );
			}, api.NotePreview.transition_duration ) );

			// Reset the left position of the edit button
			//$edit.css( 'left', 'auto' );
		} );

		// Click
		$note_sidebar_placeholder_register.on( 'click', function( event ) {
			var $this = $( this );

			// Render the modal
			api.NotePreview.views.modals.register_sidebar.modal.render( {
				command: 'note-register-sidebar',
				data: {
					post_id: $this.attr( 'data-post-id' ),
					note_sidebar_id: $this.attr( 'data-note-sidebar-id' )
				},
				// localStorage data
				localStorage: {
					key: 'ignore-register-sidebar',
					value: true
				}
			} );
		} );
	} );
} )( wp, jQuery );