+
+
+
+
+
+ ';
+
+ if( !empty($field['description']) ) {
+ ?> ( isset( $b['priority'] ) ? $b['priority'] : 10 ) ) ? 1 : -1;
+}
+
+/**
+ * Sanitize the style fields in panels_data
+ *
+ * @param $panels_data
+ *
+ * @return mixed
+ */
+function siteorigin_panels_styles_sanitize_all($panels_data){
+
+ if( !empty($panels_data['widgets']) ) {
+ // Sanitize the widgets
+ for ( $i = 0; $i < count( $panels_data['widgets'] ); $i ++ ) {
+ if ( empty( $panels_data['widgets'][ $i ]['panels_info']['style'] ) ) {
+ continue;
+ }
+ $panels_data['widgets'][ $i ]['panels_info']['style'] = siteorigin_panels_sanitize_style_fields( 'widget', $panels_data['widgets'][ $i ]['panels_info']['style'] );
+ }
+ }
+
+ if( !empty($panels_data['grids']) ) {
+ // The rows
+ for ( $i = 0; $i < count( $panels_data['grids'] ); $i ++ ) {
+ if ( empty( $panels_data['grids'][ $i ]['style'] ) ) {
+ continue;
+ }
+ $panels_data['grids'][ $i ]['style'] = siteorigin_panels_sanitize_style_fields( 'row', $panels_data['grids'][ $i ]['style'] );
+ }
+ }
+
+ if( !empty($panels_data['grid_cells']) ) {
+ // And finally, the cells
+ for ( $i = 0; $i < count( $panels_data['grid_cells'] ); $i ++ ) {
+ if ( empty( $panels_data['grid_cells'][ $i ]['style'] ) ) {
+ continue;
+ }
+ $panels_data['grid_cells'][ $i ]['style'] = siteorigin_panels_sanitize_style_fields( 'cell', $panels_data['grid_cells'][ $i ]['style'] );
+ }
+ }
+
+ return $panels_data;
+}
+
+/**
+ * Sanitize style fields.
+ *
+ * @param $section
+ * @param $styles
+ *
+ * @return Sanitized styles
+ */
+function siteorigin_panels_sanitize_style_fields($section, $styles){
+ static $fields_cache = array();
+
+ // Use the filter to get the fields for this section.
+ if( empty($fields_cache[$section]) ) {
+ $fields_cache[$section] = apply_filters('siteorigin_panels_' . $section . '_style_fields', array() );
+ }
+ $fields = $fields_cache[$section];
+
+ $return = array();
+ foreach($fields as $k => $field) {
+
+ // Ignore this if we don't even have a value for the style
+ if( !isset($styles[$k]) || $styles[$k] == '' ) continue;
+
+ switch($field['type']) {
+ case 'color' :
+ $color = $styles[$k];
+ if ( preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) ) $return[$k] = $color;
+ else $return[$k] = '';
+ break;
+ case 'image' :
+ $return[$k] = !empty( $styles[$k] ) ? intval( $styles[$k] ) : false;
+ break;
+ case 'url' :
+ $return[$k] = esc_url_raw( $styles[$k] );
+ break;
+ case 'checkbox' :
+ $return[$k] = !empty( $styles[$k] );
+ break;
+ case 'measurement' :
+ preg_match('/([0-9\.,]+)(.*)/', $styles[$k], $match);
+ if( !empty($match[0]) && $match[0] != '' && !empty($match[2]) ) $return[$k] = $styles[$k];
+ else $return[$k] = '';
+ break;
+ case 'select' :
+ if( !empty( $styles[$k] ) && in_array( $styles[$k], array_keys( $field['options'] ) ) ) {
+ $return[$k] = $styles[$k];
+ }
+ break;
+ default:
+ // Just pass the value through.
+ $return[$k] = $styles[$k];
+ break;
+
+ }
+ }
+
+ return $return;
+}
+
+/**
+ * Convert the single string attribute of the grid style into an array.
+ *
+ * @param $panels_data
+ * @return mixed
+ */
+function siteorigin_panels_style_update_data($panels_data){
+ if(empty($panels_data['grids'])) return $panels_data;
+
+ for($i = 0; $i < count($panels_data['grids']); $i++) {
+
+ if( isset($panels_data['grids'][$i]['style']) && is_string($panels_data['grids'][$i]['style']) ){
+ $panels_data['grids'][$i]['style'] = array('class' => $panels_data['grids'][$i]['style']);
+ }
+
+ }
+ return $panels_data;
+}
+add_filter('siteorigin_panels_data', 'siteorigin_panels_style_update_data');
+add_filter('siteorigin_panels_prebuilt_layout', 'siteorigin_panels_style_update_data');
\ No newline at end of file
diff --git a/inc/widgets-bundle.php b/inc/widgets-bundle.php
new file mode 100644
index 000000000..9ebe807f5
--- /dev/null
+++ b/inc/widgets-bundle.php
@@ -0,0 +1,64 @@
+ array(
+ 'class' => 'SiteOrigin_Widget_Button_Widget',
+ 'title' => __('SiteOrigin Button', 'siteorigin-panels'),
+ 'description' => __('A simple button', 'siteorigin-panels'),
+ 'installed' => false,
+ 'plugin' => array(
+ 'name' => __('SiteOrigin Widgets Bundle', 'siteorigin-panels'),
+ 'slug' => 'so-widgets-bundle'
+ ),
+ 'groups' => array('so-widgets-bundle'),
+ ),
+
+ 'SiteOrigin_Widget_Image_Widget' => array(
+ 'class' => 'SiteOrigin_Widget_Image_Widget',
+ 'title' => __('SiteOrigin Image', 'siteorigin-panels'),
+ 'description' => __('Choose images from your media library.', 'siteorigin-panels'),
+ 'installed' => false,
+ 'plugin' => array(
+ 'name' => __('SiteOrigin Widgets Bundle', 'siteorigin-panels'),
+ 'slug' => 'so-widgets-bundle'
+ ),
+ 'groups' => array('so-widgets-bundle'),
+ ),
+
+ 'SiteOrigin_Widget_Slider_Widget' => array(
+ 'class' => 'SiteOrigin_Widget_Slider_Widget',
+ 'title' => __('SiteOrigin Slider', 'siteorigin-panels'),
+ 'description' => __('A basic slider widget.', 'siteorigin-panels'),
+ 'installed' => false,
+ 'plugin' => array(
+ 'name' => __('SiteOrigin Widgets Bundle', 'siteorigin-panels'),
+ 'slug' => 'so-widgets-bundle'
+ ),
+ 'groups' => array('so-widgets-bundle'),
+ ),
+
+ 'SiteOrigin_Widget_Features_Widget' => array(
+ 'class' => 'SiteOrigin_Widget_Features_Widget',
+ 'title' => __('SiteOrigin Features', 'siteorigin-panels'),
+ 'description' => __('Display site features as a collection of icons.', 'siteorigin-panels'),
+ 'installed' => false,
+ 'plugin' => array(
+ 'name' => __('SiteOrigin Widgets Bundle', 'siteorigin-panels'),
+ 'slug' => 'so-widgets-bundle'
+ ),
+ 'groups' => array('so-widgets-bundle'),
+ ),
+
+ 'SiteOrigin_Widget_PostCarousel_Widget' => array(
+ 'class' => 'SiteOrigin_Widget_PostCarousel_Widget',
+ 'title' => __('SiteOrigin Post Carousel', 'siteorigin-panels'),
+ 'description' => __('Display your posts as a carousel.', 'siteorigin-panels'),
+ 'installed' => false,
+ 'plugin' => array(
+ 'name' => __('SiteOrigin Widgets Bundle', 'siteorigin-panels'),
+ 'slug' => 'so-widgets-bundle'
+ ),
+ 'groups' => array('so-widgets-bundle'),
+ ),
+);
\ No newline at end of file
diff --git a/inc/widgets.php b/inc/widgets.php
new file mode 100644
index 000000000..a60adb003
--- /dev/null
+++ b/inc/widgets.php
@@ -0,0 +1,152 @@
+ 'WP_Widget_Black_Studio_TinyMCE',
+ 'title' => __('Visual Editor', 'siteorigin-panels'),
+ 'description' => __('Arbitrary text or HTML with visual editor', 'siteorigin-panels'),
+ 'installed' => false,
+ 'plugin' => array(
+ 'name' => __('Black Studio TinyMCE', 'siteorigin-panels'),
+ 'slug' => 'black-studio-tinymce-widget'
+ ),
+ 'groups' => array('recommended'),
+ 'icon' => 'dashicons dashicons-edit',
+ );
+ }
+ else {
+ $widgets['WP_Widget_Black_Studio_TinyMCE']['groups'] = array('recommended');
+ $widgets['WP_Widget_Black_Studio_TinyMCE']['icon'] = 'dashicons dashicons-edit';
+ }
+
+ // Add in all the widgets bundle widgets
+ $widgets = wp_parse_args(
+ $widgets,
+ include plugin_dir_path(__FILE__).'/widgets-bundle.php'
+ );
+
+ foreach($widgets as $class => $data) {
+ if( strpos( $class, 'SiteOrigin_Panels_Widgets_' ) === 0 || strpos( $class, 'SiteOrigin_Panels_Widget_' ) === 0 ) {
+ $widgets[$class]['groups'] = array('panels');
+ }
+ }
+
+ $widgets['SiteOrigin_Panels_Widgets_Layout']['icon'] = 'dashicons dashicons-analytics';
+
+ $wordpress_widgets = array(
+ 'WP_Widget_Pages',
+ 'WP_Widget_Links',
+ 'WP_Widget_Search',
+ 'WP_Widget_Archives',
+ 'WP_Widget_Meta',
+ 'WP_Widget_Calendar',
+ 'WP_Widget_Text',
+ 'WP_Widget_Categories',
+ 'WP_Widget_Recent_Posts',
+ 'WP_Widget_Recent_Comments',
+ 'WP_Widget_RSS',
+ 'WP_Widget_Tag_Cloud',
+ 'WP_Nav_Menu_Widget',
+ );
+
+ foreach($wordpress_widgets as $wordpress_widget) {
+ if( isset( $widgets[$wordpress_widget] ) ) {
+ $widgets[$wordpress_widget]['groups'] = array('wordpress');
+ $widgets[$wordpress_widget]['icon'] = 'dashicons dashicons-wordpress';
+ }
+ }
+
+ return $widgets;
+
+}
+add_filter('siteorigin_panels_widgets', 'siteorigin_panels_add_recommended_widgets');
+
+/**
+ * Add tabs to the widget dialog
+ *
+ * @param $tabs
+ *
+ * @return array
+ */
+function siteorigin_panels_add_widgets_dialog_tabs($tabs){
+
+ $tabs[] = array(
+ 'title' => __('Widgets Bundle', 'siteorigin-panels'),
+ 'filter' => array(
+ 'groups' => array('so-widgets-bundle')
+ )
+ );
+
+ $tabs[] = array(
+ 'title' => __('Page Builder Widgets', 'siteorigin-panels'),
+ 'filter' => array(
+ 'groups' => array('panels')
+ )
+ );
+
+ $tabs[] = array(
+ 'title' => __('WordPress Widgets', 'siteorigin-panels'),
+ 'filter' => array(
+ 'groups' => array('wordpress')
+ )
+ );
+
+ $tabs[] = array(
+ 'title' => __('Recommended Widgets', 'siteorigin-panels'),
+ 'filter' => array(
+ 'groups' => array('recommended')
+ )
+ );
+
+ return $tabs;
+}
+add_filter('siteorigin_panels_widget_dialog_tabs', 'siteorigin_panels_add_widgets_dialog_tabs', 20);
+
+/**
+ * This will try restore bundled widgets.
+ *
+ * @param $object
+ * @param $widget
+ *
+ * @return \WP_Widget_Text
+ */
+function siteorigin_panels_restore_bundled_widget($object, $widget){
+
+ // We can skip this if there's already an object
+ if( !empty($object) ) return $object;
+
+ if( strpos($widget, 'SiteOrigin_Panels_Widget_') === 0 || strpos($widget, 'SiteOrigin_Panels_Widgets_') === 0 ) {
+
+ if( !class_exists('SiteOrigin_Panels_Widget') ) {
+ // Initialize the bundled widgets
+ include plugin_dir_path( SITEORIGIN_PANELS_BASE_FILE ) . '/widgets/widgets.php';
+
+ // Initialize all the widgets
+ origin_widgets_init();
+ siteorigin_panels_widgets_init();
+
+ // Set this to change the default behaviour to using the bundled widgets, wont override user settings though
+ add_option('siteorigin_panels_is_using_bundled', true);
+ }
+
+ if( class_exists($widget) ) {
+ $object = new $widget();
+ }
+ }
+ elseif(!is_admin() && $widget == 'WP_Widget_Black_Studio_TinyMCE') {
+ // If the visual editor is missing, we can replace it with the text widget for now
+ $object = new WP_Widget_Text();
+ }
+
+ return $object;
+}
+add_filter('siteorigin_panels_widget_object', 'siteorigin_panels_restore_bundled_widget', 10, 2);
\ No newline at end of file
diff --git a/js/siteorigin-panels-history.js b/js/siteorigin-panels-history.js
new file mode 100644
index 000000000..c13d56f44
--- /dev/null
+++ b/js/siteorigin-panels-history.js
@@ -0,0 +1,301 @@
+/**
+ * History browser for Page Builder.
+ *
+ * @copyright Greg Priday 2014 -
+ * @license GPL 3.0 http://www.gnu.org/licenses/gpl.html
+ */
+
+( function( $, _, panelsOptions ){
+
+ var panels = window.siteoriginPanels;
+
+ /**
+ *
+ */
+ panels.model.historyEntry = Backbone.Model.extend( {
+ defaults: {
+ text : '',
+ data : '',
+ time: null,
+ count: 1
+ }
+ } );
+
+ /**
+ *
+ */
+ panels.collection.historyEntries = Backbone.Collection.extend( {
+ model: panels.model.historyEntry,
+
+ /**
+ * The builder model
+ */
+ builder: null,
+
+ /**
+ * The maximum number of items in the history
+ */
+ maxSize: 12,
+
+ initialize: function(){
+ this.on( 'add', this.onAddEntry, this );
+ },
+
+ /**
+ * Add an entry to the collection.
+ *
+ * @param text The text that defines the action taken to get to this
+ * @param data
+ */
+ addEntry: function(text, data) {
+
+ if(typeof data == 'undefined' || data == null) {
+ data = this.builder.getPanelsData();
+ }
+
+ var entry = new panels.model.historyEntry( {
+ text: text,
+ data: JSON.stringify( data ),
+ time: parseInt( new Date().getTime() / 1000 ),
+ collection: this
+ } );
+
+ this.add( entry );
+ },
+
+ /**
+ * Resize the collection so it's not bigger than this.maxSize
+ */
+ onAddEntry: function(entry){
+
+ if(this.models.length > 1) {
+ var lastEntry = this.at(this.models.length - 2);
+
+ if(
+ ( entry.get('text') == lastEntry.get('text') && entry.get('time') - lastEntry.get('time') < 15 )
+ || ( entry.get('data') == lastEntry.get('data') )
+ ) {
+ // If both entries have the same text and are within 20 seconds of each other, or have the same data, then remove most recent
+ this.remove( entry );
+ lastEntry.set( 'count', lastEntry.get('count') + 1 );
+ }
+ }
+
+ // Make sure that there are not to many entries in this collection
+ while( this.models.length > this.maxSize ) {
+ this.shift();
+ }
+ }
+ } );
+
+ /**
+ * The history manager is
+ */
+ panels.dialog.history = panels.view.dialog.extend( {
+
+ historyEntryTemplate: _.template( $('#siteorigin-panels-dialog-history-entry').html() ),
+
+ entries: {},
+ currentEntry: null,
+ revertEntry: null,
+ selectedEntry: null,
+
+ dialogClass: 'so-panels-dialog-history',
+
+ events: {
+ 'click .so-close': 'closeDialog',
+ 'click .so-restore': 'restoreSelectedEntry'
+ },
+
+ initializeDialog: function(){
+ this.entries = new panels.collection.historyEntries();
+
+ this.on('open_dialog', this.setCurrentEntry, this);
+ this.on('open_dialog', this.renderHistoryEntries, this);
+ },
+
+ render: function(){
+ // Render the dialog and attach it to the builder interface
+ this.renderDialog( this.parseDialogContent( $('#siteorigin-panels-dialog-history').html(), {} ) );
+
+ this.$('iframe.siteorigin-panels-history-iframe').load(function(){
+ $(this).show();
+ });
+ },
+
+ /**
+ * Set the origianl entry. This should be set when creating the dialog.
+ *
+ * @param {panels.model.builder} builder
+ */
+ setRevertEntry: function(builder){
+ this.revertEntry = new panels.model.historyEntry( {
+ data: JSON.stringify( builder.getPanelsData() ),
+ time: parseInt( new Date().getTime() / 1000 )
+ } );
+ },
+
+ /**
+ * This is triggered when the dialog is opened.
+ */
+ setCurrentEntry: function(){
+ this.currentEntry = new panels.model.historyEntry( {
+ data: JSON.stringify( this.builder.model.getPanelsData() ),
+ time: parseInt( new Date().getTime() / 1000 )
+ } );
+
+ this.selectedEntry = this.currentEntry;
+ this.previewEntry( this.currentEntry );
+ this.$('.so-buttons .so-restore').addClass('disabled');
+ },
+
+ /**
+ * Render the history entries
+ */
+ renderHistoryEntries: function(){
+ var c = this.$('.history-entries');
+
+ // Set up an interval that will display the time since every 10 seconds
+ var thisView = this;
+
+ c.empty();
+
+ if( this.currentEntry.get('data') != this.revertEntry.get('data') || this.entries.models.length > 0 ) {
+ $(this.historyEntryTemplate({title: panelsOptions.loc.history['revert'], count: 1}))
+ .data('historyEntry', this.revertEntry)
+ .prependTo(c);
+ }
+
+ // Now load all the entries in this.entries
+ this.entries.each(function(entry){
+
+ var html = thisView.historyEntryTemplate( {
+ title: panelsOptions.loc.history[ entry.get('text') ],
+ count: entry.get('count')
+ } );
+
+ $( html )
+ .data('historyEntry', entry)
+ .prependTo(c);
+ });
+
+
+ $(this.historyEntryTemplate({title: panelsOptions.loc.history['current'], count: 1}))
+ .data('historyEntry', this.currentEntry)
+ .addClass('so-selected')
+ .prependTo(c);
+
+ // Handle loading and selecting
+ c.find('.history-entry').click(function(){
+ var $$ = $(this);
+ c.find('.history-entry').not($$).removeClass('so-selected');
+ $$.addClass('so-selected');
+
+ var entry = $$.data('historyEntry');
+
+ thisView.selectedEntry = entry;
+
+ if( thisView.selectedEntry.cid != thisView.currentEntry.cid ) {
+ thisView.$('.so-buttons .so-restore').removeClass('disabled');
+ }
+ else {
+ thisView.$('.so-buttons .so-restore').addClass('disabled');
+ }
+
+ thisView.previewEntry( entry );
+ });
+
+ this.updateEntryTimes();
+ },
+
+ /**
+ * Preview an entry
+ *
+ * @param entry
+ */
+ previewEntry: function(entry){
+ this.$('iframe.siteorigin-panels-history-iframe').hide();
+ this.$('form.history-form input[name="siteorigin_panels_data"]').val( entry.get('data') );
+ this.$('form.history-form').submit();
+ },
+
+ /**
+ * Restore the current entry
+ */
+ restoreSelectedEntry: function(){
+
+ if( this.$('.so-buttons .so-restore').hasClass('disabled') ) return false;
+
+ if( this.currentEntry.get('data') == this.selectedEntry.get('data') ) {
+ this.closeDialog();
+ return;
+ }
+
+ // Add an entry for this restore event
+ if( this.selectedEntry.get('text') != 'restore' ) {
+ this.entries.addEntry( 'restore', this.builder.model.getPanelsData() );
+ }
+
+ this.builder.model.loadPanelsData( JSON.parse( this.selectedEntry.get('data') ) );
+
+ this.closeDialog();
+
+ return false;
+ },
+
+ /**
+ * Update the entry times for the list of entries down the side
+ */
+ updateEntryTimes: function(){
+ var thisView = this;
+
+ this.$('.history-entries .history-entry').each(function(){
+ var $$ = $(this);
+
+ var time = $$.find('.timesince');
+ var entry = $$.data('historyEntry');
+
+ time.html( thisView.timeSince( entry.get('time') ) );
+ });
+ },
+
+ /**
+ * Gets the time since as a nice string.
+ *
+ * @param date
+ */
+ timeSince: function(time){
+ var diff = parseInt( new Date().getTime() / 1000 ) - time;
+
+ var parts = [];
+ var interval;
+
+ // There are 3600 seconds in an hour
+ if( diff > 3600 ) {
+ interval = Math.floor( diff / 3600 );
+ if(interval == 1) parts.push(panelsOptions.loc.time.hour.replace('%d', interval ));
+ else parts.push(panelsOptions.loc.time.hours.replace('%d', interval ));
+ diff -= interval * 3600
+ }
+
+ // There are 60 seconds in a minute
+ if( diff > 60 ) {
+ interval = Math.floor( diff / 60 );
+ if(interval == 1) parts.push(panelsOptions.loc.time.minute.replace('%d', interval ));
+ else parts.push(panelsOptions.loc.time.minutes.replace('%d', interval ));
+ diff -= interval * 60
+ }
+
+ if( diff > 0 ) {
+ if(diff == 1) parts.push(panelsOptions.loc.time.second.replace('%d', diff ));
+ else parts.push(panelsOptions.loc.time.seconds.replace('%d', diff ));
+ }
+
+ // Return the amount of time ago
+ return parts.length == 0 ? panelsOptions.loc.time.now : panelsOptions.loc.time.ago.replace('%s', parts.slice(0,2).join(', ') );
+
+ }
+
+ } );
+
+} )( jQuery, _, soPanelsOptions );
\ No newline at end of file
diff --git a/js/siteorigin-panels-live-editor.js b/js/siteorigin-panels-live-editor.js
new file mode 100644
index 000000000..d509d34f5
--- /dev/null
+++ b/js/siteorigin-panels-live-editor.js
@@ -0,0 +1,308 @@
+/**
+ * Handles the live editor interface in Page Builder
+ *
+ * @copyright Greg Priday 2014 -
+ * @license GPL 3.0 http://www.gnu.org/licenses/gpl.html
+ */
+
+( function( $, _, panelsOptions ){
+
+ var panels = window.siteoriginPanels;
+
+ /**
+ * Live editor handles
+ */
+ panels.view.liveEditor = Backbone.View.extend( {
+ template: _.template( $('#siteorigin-panels-live-editor').html() ),
+
+ sectionTemplate: _.template( $('#siteorigin-panels-live-editor-sidebar-section').html() ),
+
+ postId: false,
+ bodyScrollTop : null,
+ displayed: false,
+
+ events: {
+ 'click .live-editor-close': 'close'
+ },
+ frameScrollTop: 0,
+
+ initialize: function(){
+ },
+
+ /**
+ * Render the live editor
+ */
+ render: function(){
+ this.setElement( this.template() );
+ this.$el.html( this.template() );
+
+ var thisView = this;
+
+ // Prevent clicks inside the iframe
+ this.$('iframe#siteorigin-panels-live-editor-iframe')
+ .load(function(){
+ $(this).show();
+
+ var ifc = $(this).contents();
+
+ // Lets find all the first level grids. This is to account for the Page Builder layout widget.
+ ifc.find('.panel-grid .panel-grid-cell .panel.widget')
+ .filter(function(){
+ // Filter to only include non nested
+ return $(this).parents('.widget_siteorigin-panels-builder').length == 0;
+ })
+ .each(function(i, el){
+ var $$ = $(el);
+ var widgetEdit = thisView.$('.page-widgets .so-widget').eq(i);
+ var overlay;
+
+ $$
+ .css({
+ 'cursor' : 'pointer'
+ })
+ .mouseenter(function(){
+ widgetEdit.addClass('so-hovered');
+ overlay = thisView.createPreviewOverlay( $(this) )
+ })
+ .mouseleave( function(){
+ widgetEdit.removeClass('so-hovered');
+ overlay.fadeOut('fast', function(){ $(this).remove() });
+ } )
+ .click(function(e){
+ e.preventDefault();
+ // When we click a widget, send that click to the form
+ widgetEdit.click();
+ });
+ });
+
+ // Prevent default clicks
+ ifc.find( "a").css({'pointer-events' : 'none'}).click(function(e){
+ return false;
+ });
+
+ });
+ },
+
+ /**
+ * Attach the live editor to the document
+ */
+ attach: function(){
+ this.$el.appendTo('body');
+ },
+
+ setPostId: function(postId){
+ this.postId = postId;
+ },
+
+ /**
+ * Display the live editor
+ */
+ open: function(){
+ if( this.$el.html() == '' ) this.render();
+ if( this.$el.closest('body').length == 0 ) this.attach();
+
+ // Refresh the preview display
+ this.refreshWidgets();
+ this.$el.show();
+
+ // Refresh the preview after we show the editor
+ this.refreshPreview();
+
+ // Disable page scrolling
+ this.bodyScrollTop = $('body').scrollTop();
+ $('body').css( {overflow:'hidden'} );
+
+ this.displayed = true;
+ },
+
+ close: function(){
+ this.$el.hide();
+ $('body').css( {overflow:'auto'} );
+ $('body').scrollTop( this.bodyScrollTop );
+
+ this.displayed = false;
+
+ return false;
+ },
+
+ /**
+ * Refresh the preview display
+ */
+ refreshPreview: function(){
+ if( !this.$el.is(':visible') ) return;
+
+ this.$('iframe#siteorigin-panels-live-editor-iframe').hide();
+
+ this.frameScrollTop = this.$('iframe#siteorigin-panels-live-editor-iframe').contents().find('body').scrollTop();
+
+ this.$('form.live-editor-form input[name="siteorigin_panels_data"]').val( JSON.stringify( this.builder.model.getPanelsData() ) );
+ this.$('form.live-editor-form').submit();
+ },
+
+ /**
+ *
+ * @param over
+ * @return {*|Object}
+ */
+ createPreviewOverlay: function(over) {
+ var previewFrame = this.$('iframe#siteorigin-panels-live-editor-iframe');
+
+ // Remove any old overlays
+ var body = previewFrame.contents().find('body').css('position', 'relative');
+
+ previewFrame.contents().find('.panels-live-editor-overlay').remove();
+
+ // Create the new overlay
+ var overlayContainer = $('').addClass('panels-live-editor-overlay').css( {
+ 'pointer-events' : 'none'
+ } );
+
+ var overlay = $('').css({
+ 'position' : 'absolute',
+ 'background' : '#000000',
+ 'z-index' : 1000,
+ 'opacity' : 0.25
+ });
+
+ var spacing = 15;
+
+ overlayContainer
+ .append(
+ overlay.clone().css({
+ 'top' : -body.offset().top,
+ 'left' : 0,
+ 'right' : 0,
+ 'height' : over.offset().top - spacing
+ })
+ )
+ .append(
+ overlay.clone().css({
+ 'bottom' : 0,
+ 'left' : 0,
+ 'right' : 0,
+ 'height' : Math.round( body.height() - over.offset().top - over.outerHeight() - spacing + body.offset().top - 0.01 )
+ })
+ )
+ .append(
+ overlay.clone().css({
+ 'top' : over.offset().top - spacing - body.offset().top,
+ 'left' : 0,
+ 'width' : over.offset().left - spacing,
+ 'height' : Math.round(over.outerHeight() + spacing*2)
+ })
+ )
+ .append(
+ overlay.clone().css({
+ 'top' : over.offset().top - spacing - body.offset().top,
+ 'right' : 0,
+ 'left' : over.offset().left + over.outerWidth() + spacing,
+ 'height' : Math.round(over.outerHeight() + spacing*2)
+ })
+ );
+
+ // Create a new overlay
+ previewFrame.contents().find('body').append(overlayContainer);
+ return overlayContainer;
+ },
+
+ /**
+ * Refresh the widgets in the left sidebar.
+ */
+ refreshWidgets: function(){
+ // Empty all the current widgets
+ this.$('.so-sidebar .page-widgets').empty();
+ var previewFrame = this.$('iframe#siteorigin-panels-live-editor-iframe');
+
+ // Now lets move all the widgets to the sidebar
+ var thisView = this;
+ var widgetIndex = 0;
+
+ this.builder.$('.so-row-container').each(function(ri, el) {
+ var row = $(el);
+ var widgets = row.find('.so-cells .cell .so-widget');
+
+ var sectionWrapper = $( thisView.sectionTemplate({ title: 'Row ' + (ri+1) }) )
+ .appendTo( thisView.$('.so-sidebar .page-widgets') );
+
+ sectionWrapper.find('.section-header').click(function(){
+ row.data('view').editSettingsHandler();
+ });
+
+ var widgetsWrapper = sectionWrapper.find('.section-widgets');
+
+ widgets.each(function(i, el){
+ var widget = $(this);
+ var widgetClone = widget.clone().show().css({
+ opacity : 1
+ });
+
+ // Remove all the action buttons from the clone
+ widgetClone.find('.actions').remove();
+ widgetClone.find('.widget-icon').remove();
+
+ var thisWidgetIndex = (widgetIndex++);
+ var getHoverWidget = function(){
+ // TODO this should target the #pl-x selector
+ return previewFrame.contents()
+ .find('#pl-' + thisView.postId + ' .panel-grid .panel-grid-cell .panel')
+ .filter(function(){
+ // Filter to only include non nested
+ return $(this).parents('.widget_siteorigin-panels-builder').length == 0;
+ })
+ .not('panel-hover-widget').eq(thisWidgetIndex);
+ }
+
+ var overlay = null, hoverWidget = null;
+
+ widgetClone
+ .click(function(e){
+ e.preventDefault();
+ widget.data('view').editHandler();
+ return false;
+ })
+ .mouseenter(function(){
+ var hoverWidget = getHoverWidget();
+
+ // Center the iframe on the over item
+
+ previewFrame.contents()
+ .find('html,body')
+ .clearQueue()
+ .animate( {
+ scrollTop: hoverWidget.offset().top - Math.max(30, ( Math.min( previewFrame.contents().height(), previewFrame.height() ) - hoverWidget.outerHeight() ) /2 )
+ }, 750);
+
+ // Create the overlay
+ overlay = thisView.createPreviewOverlay( hoverWidget );
+
+ })
+ .mouseleave(function(){
+ // Stop any scroll animations that are currently happening
+ previewFrame.contents()
+ .find('html,body')
+ .clearQueue();
+
+ if(overlay != null) {
+ overlay.fadeOut('fast', function(){ $(this).remove() });
+ overlay = null;
+ }
+ if(hoverWidget != null) {
+ hoverWidget.remove();
+ hoverWidget = null;
+ }
+ })
+ .appendTo( widgetsWrapper );
+ });
+ });
+ },
+
+ /**
+ * Return true if the live editor has a valid preview URL.
+ * @return {boolean}
+ */
+ hasPreviewUrl: function(){
+ return this.$('form.live-editor-form').attr('action') != '';
+ }
+ } );
+
+} )( jQuery, _, soPanelsOptions );
\ No newline at end of file
diff --git a/js/siteorigin-panels-styles.js b/js/siteorigin-panels-styles.js
new file mode 100644
index 000000000..1903a2b72
--- /dev/null
+++ b/js/siteorigin-panels-styles.js
@@ -0,0 +1,169 @@
+/**
+ * @copyright Greg Priday 2014 -
+ * @license GPL 3.0 http://www.gnu.org/licenses/gpl.html
+ */
+
+( function( $, _, panelsOptions ){
+
+ var panels = window.siteoriginPanels;
+
+
+ /**
+ * The styles view handlers all the cool rendering stuff
+ */
+ panels.view.styles = Backbone.View.extend( {
+
+ stylesLoaded: false,
+
+ initialize: function(){
+
+ },
+
+ /**
+ * Render the visual styles object.
+ *
+ * @param type
+ */
+ render: function( stylesType ){
+ if( typeof stylesType == 'undefined' ) return false;
+
+ this.$el.addClass('so-visual-styles');
+
+ // Load the form
+ var thisView = this;
+ $.post(
+ ajaxurl,
+ {
+ action: 'so_panels_style_form',
+ type: stylesType,
+ style: this.model.get('style')
+ },
+ function( response ){
+ thisView.$el.html( response );
+ thisView.setupFields();
+ thisView.stylesLoaded = true;
+ thisView.trigger('styles_loaded');
+ }
+ );
+ },
+
+ /**
+ * Attach the style view to the DOM.
+ *
+ * @param wrapper
+ */
+ attach: function( wrapper ){
+ wrapper.append( this.$el );
+ },
+
+ /**
+ * Detach the styles view from the DOM
+ */
+ detach: function(){
+ this.$el.detach();
+ },
+
+ /**
+ * Setup all the fields
+ */
+ setupFields: function(){
+
+ // Set up the sections as collapsible
+ this.$('.style-section-wrapper').each(function(){
+ var $s = $(this);
+
+ $s.find('.style-section-head').click( function(e){
+ e.preventDefault();
+ $s.find('.style-section-fields').slideToggle('fast');
+ } );
+ });
+
+ // Set up the color fields
+ if(typeof $.fn.wpColorPicker != 'undefined') {
+ this.$('.so-wp-color-field').wpColorPicker();
+ }
+
+ // Set up the image select fields
+ this.$('.style-field-image').each( function(){
+ var frame = null;
+ var $s = $(this);
+
+ $s.find('.so-image-selector').click( function(){
+
+ if( frame == null ) {
+ // Create the media frame.
+ frame = wp.media({
+ // Set the title of the modal.
+ title: 'choose',
+
+ // Tell the modal to show only images.
+ library: {
+ type: 'image'
+ },
+
+ // Customize the submit button.
+ button: {
+ // Set the text of the button.
+ text: 'Done',
+ close: true
+ }
+ });
+
+ frame.on( 'select', function(){
+ var attachment = frame.state().get('selection').first().attributes;
+
+ try {
+ $s.find( '.current-image' ).css( 'background-image', 'url(' + attachment.sizes.thumbnail.url + ')' );
+ }
+ catch(e) {
+ // We'll use the full image instead
+ $s.find( '.current-image' ).css( 'background-image', 'url(' + attachment.sizes.full.url + ')' );
+ }
+
+ // Store the ID
+ $s.find('input').val( attachment.id )
+ } );
+ }
+
+ frame.open();
+
+ } );
+
+ // Handle clicking on remove
+ $s.find('.remove-image').click(function(){
+ $s.find( '.current-image').css('background-image', 'none');
+ $s.find('input').val( '' )
+ });
+ } );
+
+ // Set up all the measurement fields
+ this.$('.style-field-measurement').each(function(){
+ var $$ = $(this);
+
+ var text = $$.find('input[type="text"]');
+ var unit = $$.find('select');
+ var hidden = $$.find('input[type="hidden"]');
+
+ // Load the value from the hidden field
+ if( hidden.val() != '' ) {
+ var re = /([0-9\.,]+)(.*)/;
+ var match = re.exec( hidden.val() );
+ if( match != null && typeof match[1] != 'undefined' && typeof match[2] != 'undefined' ) {
+ text.val( match[1] );
+ unit.val( match[2] );
+ }
+ }
+
+ var setVal = function(){
+ hidden.val( text.val() + unit.val() );
+ };
+
+ // Set the value when ever anything changes
+ text.keyup(setVal).change(setVal);
+ unit.change(setVal);
+ } );
+ }
+
+ } );
+
+} )( jQuery, _, soPanelsOptions );
\ No newline at end of file
diff --git a/js/siteorigin-panels.js b/js/siteorigin-panels.js
new file mode 100644
index 000000000..0dc1c2bed
--- /dev/null
+++ b/js/siteorigin-panels.js
@@ -0,0 +1,3281 @@
+/**
+ * Everything we need for SiteOrigin Page Builder.
+ *
+ * @copyright Greg Priday 2013 - 2014 -
+ * @license GPL 3.0 http://www.gnu.org/licenses/gpl.html
+ */
+
+( function( $, _, panelsOptions ){
+
+ var panels = {
+ model : { },
+ collection : { },
+ view : { },
+ dialog : { },
+ fn : {}
+ };
+
+ /**
+ * Model for an instance of a widget
+ */
+ panels.model.widget = Backbone.Model.extend( {
+
+ cell: null,
+
+ defaults: {
+ // The PHP Class of the widget
+ class : null,
+
+ // Is this class missing?
+ missing : false,
+
+ // The values of the widget
+ values: {},
+
+ // Have the current values been passed through the widgets update function
+ raw: false,
+
+ // Visual style fields
+ styles: {}
+ },
+
+ initialize: function(){
+ },
+
+ /**
+ * @param field
+ * @returns {*}
+ */
+ getWidgetField: function(field){
+ if(typeof panelsOptions['widgets'][this.get('class')] == 'undefined') {
+ if(field == 'title' || field == 'description') return panelsOptions.loc.missing_widget[field];
+ else return '';
+ }
+ else return panelsOptions['widgets'][this.get('class')][field];
+ },
+
+ /**
+ * Move this widget to a new cell
+ *
+ * @param panels.model.cell newCell
+ */
+ moveToCell: function(newCell){
+ if( this.cell.cid == newCell.cid ) return;
+
+ this.cell = newCell;
+ this.collection.remove(this, {silent:true});
+ newCell.widgets.add(this, {silent:true});
+ },
+
+ /**
+ * Trigger an event on the model that indicates a user wants to edit it
+ */
+ triggerEdit: function(){
+ this.trigger('user_edit', this);
+ },
+
+ /**
+ * Trigger an event on the widget that indicates a user wants to duplicate it
+ */
+ triggerDuplicate: function(){
+ this.trigger('user_duplicate', this);
+ },
+
+ /**
+ * This is basically a wrapper for set that checks if we need to trigger a change
+ */
+ setValues: function(values){
+ var hasChanged = false;
+ if( JSON.stringify( values ) != JSON.stringify( this.get('values') ) ) {
+ hasChanged = true;
+ }
+
+ this.set( 'values', values, {silent: true} );
+
+ if( hasChanged ) {
+ // We'll trigger our own change events
+ this.trigger('change');
+ this.trigger('change:values')
+ }
+ },
+
+ /**
+ * Create a clone of this widget attached to the given cell.
+ *
+ * @param {panels.model.cell} cell
+ * @returns {panels.model.widget}
+ */
+ clone: function( cell, options ){
+ if( typeof cell == 'undefined' ) cell = this.cell;
+
+ var clone = new this.constructor( this.attributes );
+
+ // Create a deep clone of the original values
+ var cloneValues = JSON.parse( JSON.stringify( this.get('values') ) );
+
+ if( this.get('class') == "SiteOrigin_Panels_Widgets_Layout" ) {
+ // Special case of this being a layout widget, it needs a new ID
+ cloneValues.builder_id = Math.random().toString(36).substr(2);
+ }
+
+ clone.set( 'values', cloneValues, { silent: true } );
+ clone.set( 'collection', cell.widgets, { silent: true } );
+ clone.cell = cell;
+ clone.isDuplicate = true;
+ return clone;
+ },
+
+ /**
+ * Gets the value that makes most sense as the title.
+ */
+ getTitle: function(){
+ var widgetData = panelsOptions.widgets[this.get('class')];
+ if( typeof widgetData.panels_title != 'undefined' ) {
+ // This means that the widget has told us which field it wants us to use as a title
+ if( widgetData.panels_title === false ) {
+ return panelsOptions.widgets[this.get('class')].description;
+ }
+ }
+
+ var values = this.get('values');
+ var thisModel = this;
+
+ // Create a list of fields to check for a title
+ var titleFields = ['title', 'text'];
+ for (var k in values){
+ titleFields.push( k );
+ }
+ titleFields = _.uniq(titleFields);
+
+ for( var i in titleFields ) {
+ if(
+ typeof values[titleFields[i]] != 'undefined' &&
+ typeof values[titleFields[i]] == 'string' &&
+ values[titleFields[i]] != '' &&
+ !$.isNumeric( values[titleFields[i]] )
+ ) {
+ var title = values[ titleFields[i] ];
+ title = title.replace(/<\/?[^>]+(>|$)/g, "");
+ var parts = title.split(" ");
+ parts = parts.slice(0, 20);
+ return parts.join(' ');
+ }
+ }
+
+ // If we still have nothing, then just return the widget description
+ return this.getWidgetField('description');
+ }
+
+ } );
+
+ /**
+ * The view for a widget in the builder interface
+ */
+ panels.view.widget = Backbone.View.extend({
+ template: _.template( $('#siteorigin-panels-builder-widget').html() ),
+
+ // The cell view that
+ cell: null,
+
+ dialog: null,
+
+ events: {
+ 'click .widget-edit' : 'editHandler',
+ 'click .title h4' : 'editHandler',
+ 'click .actions .widget-duplicate' : 'duplicateHandler',
+ 'click .actions .widget-delete' : 'deleteHandler'
+ },
+
+ /**
+ * Initialize the widget
+ */
+ initialize: function(){
+ // The 2 user actions on the model that this view will handle.
+ this.model.on('user_edit', this.editHandler, this);
+ this.model.on('user_duplicate', this.duplicateHandler, this);
+ this.model.on('destroy', this.onModelDestroy, this);
+ this.model.on('visual_destroy', this.visualDestroyModel, this);
+
+ this.model.on('change:values', this.onModelChange, this);
+ },
+
+ /**
+ * Render the widget
+ */
+ render: function(options){
+ options = _.extend({'loadForm': false}, options);
+
+ this.setElement( this.template( {
+ title : this.model.getWidgetField('title'),
+ description : this.model.getTitle()
+ } ) );
+
+ this.$el.data( 'view', this );
+
+ if( _.size( this.model.get('values') ) == 0 || options.loadForm) {
+ // If this widget doesn't have a value, create a form and save it
+ var dialog = this.getEditDialog();
+
+ // Save the widget as soon as the form is loaded
+ dialog.once('form_loaded', dialog.saveWidget, dialog);
+
+ // Setup the dialog to load the form
+ dialog.setupDialog();
+ }
+ },
+
+ /**
+ * Display an animation that implies creation using a visual animation
+ */
+ visualCreate: function(){
+ this.$el.hide().fadeIn( 'fast' );
+ },
+
+ /**
+ * Get the dialog view of the form that edits this widget
+ *
+ * @returns {null}
+ */
+ getEditDialog: function(){
+ if(this.dialog == null){
+ this.dialog = new panels.dialog.widget({
+ model: this.model
+ });
+ this.dialog.setBuilder(this.cell.row.builder);
+
+ // Store the widget view
+ this.dialog.widgetView = this;
+ }
+ return this.dialog;
+ },
+
+ /**
+ * Handle clicking on edit widget.
+ *
+ * @returns {boolean}
+ */
+ editHandler: function(){
+ // Create a new dialog for editing this
+ this.getEditDialog().openDialog();
+ return false;
+ },
+
+ /**
+ * Handle clicking on duplicate.
+ *
+ * @returns {boolean}
+ */
+ duplicateHandler: function(){
+ // Add the history entry
+ this.cell.row.builder.addHistoryEntry('widget_duplicated');
+
+ // Create the new widget and connect it to the widget collection for the current row
+ var newWidget = this.model.clone( this.model.cell );
+
+ this.cell.model.widgets.add(newWidget, {
+ // Add this after the existing model
+ at: this.model.collection.indexOf( this.model ) + 1
+ });
+
+ return false;
+ },
+
+ /**
+ * Handle clicking on delete.
+ *
+ * @returns {boolean}
+ */
+ deleteHandler: function(){
+ this.model.trigger('visual_destroy');
+ return false;
+ },
+
+ onModelChange: function(){
+ // Update the description when ever the model changes
+ this.$('.description').html( this.model.getTitle() );
+ },
+
+ /**
+ * When the model is destroyed, fade it out
+ */
+ onModelDestroy: function(){
+ this.remove();
+ },
+
+ /**
+ * Visually destroy a model
+ */
+ visualDestroyModel: function(){
+ // Add the history entry
+ this.cell.row.builder.addHistoryEntry('widget_deleted');
+
+ var thisView = this;
+ this.$el.fadeOut('fast', function(){
+ thisView.cell.row.resize();
+ thisView.model.destroy();
+ } );
+ }
+
+ });
+
+ /**
+ * A collection of widgets, most often used for cells
+ */
+ panels.collection.widgets = Backbone.Collection.extend( {
+ model : panels.model.widget,
+
+ initialize: function(){
+ }
+
+ } );
+
+ /**
+ * A cell is a collection of widget instances
+ */
+ panels.model.cell = Backbone.Model.extend( {
+ /* A collection of widgets */
+ widgets: {},
+
+ /* The row this model belongs to */
+ row: null,
+
+ defaults: {
+ weight : 0
+ },
+
+ /**
+ * Set up the cell model
+ */
+ initialize: function(){
+ this.widgets = new panels.collection.widgets();
+ this.on('destroy', this.onDestroy, this);
+ },
+
+ /**
+ * Triggered when we destroy a cell
+ */
+ onDestroy: function(){
+ _.invoke(this.widgets.toArray(), 'destroy');
+ this.widgets.reset();
+ },
+
+ /**
+ * Create a clone of the cell, along with all its widgets
+ */
+ clone: function(row, cloneOptions){
+ if( typeof row == 'undefined' ) row = this.row;
+ cloneOptions = _.extend({ cloneWidgets: true }, cloneOptions);
+
+ var clone = new this.constructor( this.attributes );
+ clone.set('collection', row.cells, {silent: true});
+ clone.row = row;
+
+ if( cloneOptions.cloneWidgets ) {
+ // Now we're going add all the widgets that belong to this, to the clone
+ this.widgets.each(function(widget){
+ clone.widgets.add( widget.clone( clone, cloneOptions ), {silent: true} );
+ });
+ }
+
+ return clone;
+ }
+
+ } );
+
+ /**
+ * A cell collection is used to represent a row
+ */
+ panels.collection.cells = Backbone.Collection.extend( {
+ model: panels.cell,
+
+ initialize: function(){
+ this.on('add', this.onAddCell, this);
+ },
+
+ /**
+ * Get the total weight for the cells in this collection.
+ * @returns {number}
+ */
+ totalWeight: function(){
+ var totalWeight = 0;
+ this.each(function(cell){
+ totalWeight += cell.get('weight');
+ });
+
+ return totalWeight;
+ }
+ } );
+
+ /**
+ * The view for a cell
+ */
+ panels.view.cell = Backbone.View.extend( {
+ template: _.template( $('#siteorigin-panels-builder-cell').html() ),
+ events : {
+ 'click .cell-wrapper' : 'handleCellClick',
+ 'click .so-cell-actions a' : 'handleActionClick'
+ },
+
+ /* The row view that this cell is a part of */
+ row: null,
+ widgetSortable: null,
+
+ initialize: function(){
+ this.model.widgets.on('add', this.onAddWidget, this);
+ },
+
+ /**
+ * Render the actual cell
+ */
+ render: function(){
+ var templateArgs = {
+ weight: this.model.get('weight'),
+ totalWeight: this.row.model.cells.totalWeight()
+ };
+
+ this.setElement( this.template(templateArgs) );
+ this.$el.data('view', this);
+
+ // Now lets render any widgets that are currently in the row
+ var thisView = this;
+ this.model.widgets.each(function(widget){
+ var widgetView = new panels.view.widget( { model: widget } );
+ widgetView.cell = thisView;
+ widgetView.render();
+
+ widgetView.$el.appendTo( thisView.$('.widgets-container') );
+ });
+
+ this.initSortable();
+ this.initResizable();
+ },
+
+ /**
+ * Initialize the widget sortable
+ */
+ initSortable: function(){
+ var cellView = this;
+
+ var builderID = cellView.row.builder.$el.attr('id');
+
+ var scrollTop;
+
+ // Create a widget sortable that's connected with all other cells
+ this.widgetSortable = this.$el.find('.widgets-container').sortable( {
+ placeholder: "so-widget-sortable-highlight",
+ connectWith: '#' + builderID + ' .so-cells .cell .widgets-container',
+ tolerance:'pointer',
+ scroll: false,
+ over: function(e, ui){
+ // This will make all the rows in the current builder resize
+ cellView.row.builder.trigger('widget_sortable_move');
+ },
+ stop: function(e, ui){
+ cellView.row.builder.addHistoryEntry('widget_moved');
+
+ var widget = $(ui.item).data('view');
+ var targetCell = $(ui.item).closest('.cell').data('view');
+
+ // Move the model and the view to the new cell
+ widget.model.moveToCell( targetCell.model );
+ widget.cell = targetCell;
+
+ cellView.row.builder.sortCollections();
+ },
+ helper: function(e, el){
+ var helper = el.clone().css('width', el.outerWidth()).addClass('widget-being-dragged').appendTo( 'body' );
+
+ // Center the helper to the mouse cursor.
+ if( el.outerWidth() > 720 ) {
+ helper.animate({
+ 'margin-left': e.pageX - el.offset().left - (480 / 2),
+ 'width': 480
+ }, 'fast');
+ }
+
+ return helper;
+ }
+ } );
+ },
+
+ /**
+ * Refresh the widget sortable when a new widget is added
+ */
+ refreshSortable: function(){
+ this.widgetSortable.sortable('refresh');
+ },
+
+ /**
+ * This will make the cell resizble
+ */
+ initResizable: function(){
+ // var neighbor = this.$el.previous().data('view');
+ var handle = this.$('.resize-handle').css('position', 'absolute');
+ var container = this.row.$el;
+ var cellView = this;
+
+ // The view of the cell to the left is stored when dragging starts.
+ var previousCell;
+
+ handle.draggable({
+ axis: 'x',
+ containment: container,
+ start: function(e, ui){
+ // Set the containment to the cell parent
+ previousCell = cellView.$el.prev().data('view');
+ if( typeof previousCell == 'undefined' ) return false;
+
+ // Create the clone for the current cell
+ var newCellClone = cellView.$el.clone().appendTo(ui.helper).css({
+ position : 'absolute',
+ top : '0',
+ width : cellView.$el.outerWidth(),
+ left : 5,
+ height: cellView.$el.outerHeight()
+ });
+ newCellClone.find('.resize-handle').remove();
+
+ // Create the clone for the previous cell
+ var prevCellClone = previousCell.$el.clone().appendTo(ui.helper).css({
+ position : 'absolute',
+ top : '0',
+ width : previousCell.$el.outerWidth(),
+ right : 5,
+ height: previousCell.$el.outerHeight()
+ });
+ prevCellClone.find('.resize-handle').remove();
+
+ $(this).data({
+ 'newCellClone' : newCellClone,
+ 'prevCellClone' : prevCellClone
+ });
+ },
+ drag: function(e, ui){
+ // Calculate the new cell and previous cell widths as a percent
+ var containerWidth = cellView.row.$el.width() + 10;
+ var ncw = cellView.model.get('weight') - ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
+ var pcw = previousCell.model.get('weight') + ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
+
+ $(this).data('newCellClone').css('width', containerWidth * ncw )
+ .find('.preview-cell-weight').html( Math.round(ncw*1000)/10 );
+
+ $(this).data('prevCellClone').css('width', containerWidth * pcw )
+ .find('.preview-cell-weight').html( Math.round(pcw*1000)/10 );
+ },
+ stop: function(e, ui){
+ // Remove the clones
+ $(this).data('newCellClone').remove();
+ $(this).data('prevCellClone').remove();
+
+ var containerWidth = cellView.row.$el.width() + 10;
+ var ncw = cellView.model.get('weight') - ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
+ var pcw = previousCell.model.get('weight') + ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
+
+ if( ncw > 0.02 && pcw > 0.02 ) {
+ cellView.row.builder.addHistoryEntry('cell_resized');
+ cellView.model.set('weight', ncw);
+ previousCell.model.set('weight', pcw);
+ cellView.row.resize();
+ }
+
+ ui.helper.css('left', -handle.outerWidth()/2);
+ }
+ });
+
+ },
+
+ /**
+ * This is triggered when ever a widget is added to the row collection.
+ *
+ * @param widget
+ */
+ onAddWidget: function(widget, collection, options){
+ options = _.extend({noAnimate : false}, options);
+
+ // Create the view for the widget
+ var view = new panels.view.widget( {
+ model: widget
+ } );
+ view.cell = this;
+
+ if( typeof widget.isDuplicate == 'undefined' ) {
+ widget.isDuplicate = false;
+ }
+
+ // Render and load the form if this is a duplicate
+ view.render({
+ 'loadForm': widget.isDuplicate
+ });
+
+ if( typeof options.at == 'undefined' || collection.length <= 1 ) {
+ // Insert this at the end of the widgets container
+ view.$el.appendTo( this.$( '.widgets-container' ) );
+ }
+ else {
+ // We need to insert this at a specific position
+ view.$el.insertAfter(
+ this.$('.widgets-container .so-widget').eq( options.at - 1 )
+ );
+ }
+
+ if( options.noAnimate === false ) {
+ // We need an animation
+ view.visualCreate();
+ }
+
+ this.refreshSortable();
+ this.row.resize();
+ },
+
+ /**
+ * Handle an action click on this cell
+ *
+ * @param e
+ * @returns {boolean}
+ */
+ handleActionClick : function(e){
+ return false;
+ }
+ } );
+
+ /**
+ * Model for a row of cells
+ */
+ panels.model.row = Backbone.Model.extend( {
+ /* A collection of the cells in this row */
+ cells: {},
+
+ /* The builder model */
+ builder: null,
+
+ defaults :{
+ style: {}
+ },
+
+ /**
+ * Initialize the row model
+ */
+ initialize: function(){
+ this.cells = new panels.collection.cells();
+ this.on('destroy', this.onDestroy, this);
+ },
+
+ /**
+ * Add cells to the model row
+ *
+ * @param cells an array of cells, where each object in the array has a weight value
+ * @todo make this handle changing the number of cells in an existing row
+ */
+ setCells: function(cells){
+ var thisModel = this;
+
+ if( this.cells.length == 0 ) {
+ // We're adding the initial cells
+ _.each(cells, function (cellWeight) {
+ // Add the new cell to the row
+ var cell = new panels.model.cell({
+ weight: cellWeight,
+ collection: thisModel.cells
+ });
+ cell.row = thisModel;
+ thisModel.cells.add(cell);
+ });
+ }
+ else {
+
+ if(cells.length > this.cells.length) {
+ // We need to add cells
+ for( var i = this.cells.length; i < cells.length; i++ ) {
+ var cell = new panels.model.cell({
+ weight: cells[ cells.length + i ],
+ collection: thisModel.cells
+ });
+ cell.row = this;
+ thisModel.cells.add(cell);
+ }
+
+ }
+ else if(cells.length < this.cells.length) {
+ // We need to remove cells
+ _.each(this.cells.slice( cells.length, this.cells.length), function(cell){
+ cell.destroy();
+ });
+ }
+
+ // Now we need to change the weights of all the cells
+ this.cells.each(function(cell, i){
+ cell.set('weight', cells[i]);
+ });
+ }
+
+ // Rescale the cells when we add or remove
+ this.reweightCells();
+ },
+
+ /**
+ * Make sure that all the cell weights add up to 1
+ */
+ reweightCells: function() {
+ var totalWeight = 0;
+ this.cells.each( function(cell){
+ totalWeight += cell.get('weight');
+ } );
+
+ this.cells.each( function(cell){
+ cell.set( 'weight', cell.get('weight') / totalWeight );
+ } );
+
+ // This is for the row view to hook into and resize
+ this.trigger('reweight_cells');
+ },
+
+ /**
+ * Triggered when the model is destroyed
+ */
+ onDestroy: function(){
+ // Also destroy all the cells
+ _.invoke(this.cells.toArray(), 'destroy');
+ this.cells.reset();
+ },
+
+ /**
+ * Create a clone of the row, along with all its cells
+ *
+ * @param {panels.model.builder} builder The builder model to attach this to.
+ *
+ * @return {panels.model.row} The cloned row.
+ */
+ clone: function( builder, cloneOptions ){
+ if(typeof builder == 'undefined') builder = this.builder;
+ cloneOptions = _.extend({ cloneCells: true }, cloneOptions);
+
+ var clone = new this.constructor( this.attributes );
+ clone.set('collection', builder.rows, {silent: true});
+ clone.builder = builder;
+
+ if( cloneOptions.cloneCells ) {
+ // Clone all the rows
+ this.cells.each(function(cell){
+ clone.cells.add( cell.clone( clone, cloneOptions ), {silent: true});
+ });
+ }
+
+ return clone;
+ }
+ } );
+
+ /**
+ * A collection of rows. This is used to represent the entire content of Page Builder.
+ */
+ panels.collection.rows = Backbone.Collection.extend( {
+ model: panels.model.row,
+
+ initialize: function(){
+ },
+
+ /**
+ * Destroy all the rows in this collection
+ */
+ empty: function(){
+ var model;
+ while ( model = this.collection.first() ) {
+ model.destroy();
+ }
+ }
+ } );
+
+ /**
+ * View for handling the row.
+ */
+ panels.view.row = Backbone.View.extend( {
+ template: _.template( $('#siteorigin-panels-builder-row').html() ),
+
+ events: {
+ 'click .so-row-settings' : 'editSettingsHandler',
+ 'click .so-row-duplicate' : 'duplicateHandler',
+ 'click .so-row-delete' : 'confirmedDeleteHandler'
+ },
+
+ builder: null,
+ dialog: null,
+
+ /**
+ * Initialize the row view
+ */
+ initialize: function(){
+
+ this.model.cells.on('add', this.handleCellAdd, this);
+ this.model.cells.on('remove', this.handleCellRemove, this);
+ this.model.on('reweight_cells', this.resize, this);
+
+ this.model.on('destroy', this.onModelDestroy, this);
+ this.model.on('visual_destroy', this.visualDestroyModel, this);
+
+ var thisView = this;
+ this.model.cells.each(function(cell){
+ thisView.listenTo(cell.widgets, 'add', thisView.resize);
+ });
+
+ // When ever a new cell is added, listen to it for new widgets
+ this.model.cells.on('add', function(cell){
+ thisView.listenTo(cell.widgets, 'add', thisView.resize);
+ }, this);
+
+ },
+
+ /**
+ * Render the row.
+ *
+ * @returns {panels.view.row}
+ */
+ render: function(){
+ this.setElement( this.template() );
+ this.$el.data('view', this);
+
+ // Create views for the cells in this row
+ var thisView = this;
+ this.model.cells.each( function(cell){
+ var cellView = new panels.view.cell({
+ model: cell
+ });
+ cellView.row = thisView;
+ cellView.render();
+ cellView.$el.appendTo( thisView.$('.so-cells') );
+ } );
+
+ // Resize the rows when ever the widget sortable moves
+ this.builder.on('widget_sortable_move', this.resize, this);
+ this.builder.on('builder_resize', this.resize, this);
+
+ this.resize();
+
+ return this;
+ },
+
+ /**
+ * Give a visual indication of the creation of this row
+ */
+ visualCreate: function(){
+ this.$el.hide().fadeIn('fast');
+ },
+
+ /**
+ * Visually resize the row so that all cell heights are the same and the widths so that they balance to 100%
+ *
+ * @param e
+ */
+ resize: function(e){
+ // Don't resize this
+ if( !this.$el.is(':visible') ) return;
+
+ // Reset everything to have an automatic height
+ this.$el.find( '.so-cells .cell-wrapper' ).css( 'min-height', 0 );
+
+ // We'll tie the values to the row view, to prevent issue with values going to different rows
+ var height = 0;
+ this.$el.find('.so-cells .cell').each( function () {
+ height = Math.max(
+ height,
+ $(this ).height()
+ );
+
+ $( this ).css( 'width', ( $(this).data('view').model.get('weight') * 100 ) + "%" );
+ } );
+
+ // Resize all the grids and cell wrappers
+ this.$el.find( '.so-cells .cell-wrapper' ).css( 'min-height', Math.max( height, 70 ) );
+ },
+
+ /**
+ * Remove the view from the dom.
+ */
+ onModelDestroy: function() {
+ this.remove();
+ },
+
+ /**
+ * Fade out the view and destroy the model
+ */
+ visualDestroyModel: function(){
+ this.builder.addHistoryEntry('row_deleted');
+ var thisView = this;
+ this.$el.fadeOut('normal', function(){
+ thisView.model.destroy();
+ thisView.builder.model.refreshPanelsData();
+
+ if(thisView.builder.liveEditor.displayed) {
+ thisView.builder.liveEditor.refreshWidgets();
+ }
+ });
+ },
+
+ /**
+ * Duplicate this row.
+ *
+ * @return {boolean}
+ */
+ duplicateHandler: function(){
+ this.builder.addHistoryEntry('row_duplicated');
+
+ var duplicateRow = this.model.clone( this.builder.model );
+
+ this.builder.model.rows.add( duplicateRow, {
+ at: this.builder.model.rows.indexOf( this.model ) + 1
+ } );
+
+ return false;
+ },
+
+ /**
+ * Handles deleting the row with a confirmation.
+ */
+ confirmedDeleteHandler: function(e){
+ var $$ = $(e.target);
+
+ // The user clicked on the dashicon
+ if( $$.hasClass('dashicons') ) $$ = $$.parent();
+
+ if( $$.hasClass('so-confirmed') ) {
+ this.visualDestroyModel();
+ }
+ else {
+ var originalText = $$.html();
+
+ $$.addClass('so-confirmed').html(
+ '' + panelsOptions.loc.dropdown_confirm
+ );
+
+ setTimeout(function(){
+ $$.removeClass('so-confirmed').html(originalText);
+ }, 2500);
+ }
+
+ return false;
+ },
+
+ /**
+ * Handle displaying the settings dialog
+ */
+ editSettingsHandler: function(){
+ // Lets open up an instance of the settings dialog
+ var dialog = this.builder.dialogs.row;
+
+ if( this.dialog == null ) {
+ // Create the dialog
+ this.dialog = new panels.dialog.row();
+ this.dialog.setBuilder( this.builder).setRowModel( this.model);
+ }
+
+ this.dialog.openDialog();
+
+ return false;
+ },
+
+ /**
+ * Handle deleting this entire row.
+ */
+ deleteHandler: function(){
+ this.model.destroy();
+ return false;
+ },
+
+ /**
+ * Handle a new cell being added to this row view. For now we'll assume the new cell is always last
+ */
+ handleCellAdd: function(cell){
+ var cellView = new panels.view.cell({
+ model: cell
+ });
+ cellView.row = this;
+ cellView.render();
+ cellView.$el.appendTo( this.$('.so-cells') );
+ },
+
+ /**
+ * Handle a cell being removed from this row view
+ */
+ handleCellRemove: function(cell){
+ // Find the view that ties in to the cell we're removing
+ this.$el.find('.so-cells > .cell').each( function(){
+ var view = $(this).data('view');
+ if(typeof view == 'undefined') return;
+
+ if( view.model.cid == cell.cid ) {
+ // Remove this view
+ view.remove();
+ }
+ } );
+ }
+
+ } );
+
+ /**
+ * The builder model
+ */
+ panels.model.builder = Backbone.Model.extend( {
+ rows: {},
+
+ defaults : {
+ 'data' : {
+ 'widgets' : [],
+ 'grids' : [],
+ 'grid_cells' : []
+ }
+ },
+
+ initialize: function(){
+ // These are the main rows in the interface
+ this.rows = new panels.collection.rows();
+ },
+
+ /**
+ * Add a new row to this builder.
+ *
+ * @param weights
+ */
+ addRow: function( weights, options ){
+ options = _.extend({noAnimate : false}, options);
+ // Create the actual row
+ var row = new panels.model.row( {
+ collection: this.rows
+ } );
+ row.setCells( weights );
+ row.builder = this;
+ this.rows.add(row, options);
+
+ return row;
+ },
+
+ /**
+ * Load the panels data into the builder
+ *
+ * @param data
+ */
+ loadPanelsData: function(data){
+ // Start by destroying any rows that currently exist. This will in turn destroy cells, widgets and all the associated views
+ this.emptyRows();
+
+ // This will empty out the current rows and reload the builder data.
+ this.set( 'data', data, {silent: true} );
+
+ var cit = 0;
+ var rows = [];
+
+ if( typeof data.grid_cells == 'undefined' ) return;
+
+ var gi;
+ for(var ci = 0; ci < data.grid_cells.length; ci++) {
+ gi = parseInt(data.grid_cells[ci].grid);
+ if(typeof rows[gi] == 'undefined') rows[gi] = [];
+
+ rows[gi].push( parseFloat( data.grid_cells[ci].weight ) );
+ }
+
+ var builderModel = this;
+ _.each( rows, function(row, i){
+ // This will create and add the row model and its cells
+ var row = builderModel.addRow( row, { noAnimate: true } );
+
+ if( typeof data.grids[i].style != 'undefined' ) {
+ row.set( 'style', data.grids[i].style );
+ }
+ } );
+
+
+ if( typeof data.widgets == 'undefined' ) return;
+
+ // Add the widgets
+ _.each(data.widgets, function(widgetData){
+ try {
+ var panels_info = null;
+ if (typeof widgetData.panels_info != 'undefined') {
+ panels_info = widgetData.panels_info;
+ delete widgetData.panels_info;
+ }
+ else {
+ panels_info = widgetData.info;
+ delete widgetData.info;
+ }
+
+ var row = builderModel.rows.at( parseInt(panels_info.grid) );
+ var cell = row.cells.at(parseInt(panels_info.cell));
+
+ var newWidget = new panels.model.widget({
+ class: panels_info.class,
+ values: widgetData
+ });
+
+ if( typeof panels_info.style != 'undefined' ) {
+ newWidget.set('style', panels_info.style );
+ }
+
+ newWidget.cell = cell;
+ cell.widgets.add(newWidget, {noAnimate: true});
+ }
+ catch (err) {
+ // TODO handle this error
+ }
+ } );
+ },
+
+ /**
+ * Convert the content of the builder into a object that represents the page builder data
+ */
+ getPanelsData: function(){
+
+ var data = {
+ 'widgets' : [],
+ 'grids' : [],
+ 'grid_cells' : []
+ };
+ var widgetId = 0;
+
+ this.rows.each(function(row, ri){
+
+ row.cells.each(function(cell, ci){
+
+ cell.widgets.each(function(widget, wi){
+ // Add the data for the widget, including the panels_info field.
+ var values = _.extend( _.clone( widget.get('values') ), {
+ panels_info : {
+ class: widget.get('class'),
+ raw: widget.get('raw'),
+ grid: ri,
+ cell: ci,
+ id: widgetId++,
+ style: widget.get('style')
+ }
+ } );
+ data.widgets.push( values );
+ });
+
+ // Add the cell info
+ data.grid_cells.push( {
+ grid: ri,
+ weight: cell.get('weight')
+ } );
+
+ });
+
+ data.grids.push( {
+ cells: row.cells.length,
+ style: row.get('style')
+ } );
+
+ } );
+
+ return data;
+
+ },
+
+ /**
+ * This will check all the current entries and refresh the panels data
+ */
+ refreshPanelsData: function(){
+ var oldData = JSON.stringify( this.get('data') );
+ var newData = this.getPanelsData();
+ this.set( 'data', newData, { silent: true } );
+
+ if( JSON.stringify( newData ) != oldData ) {
+ // The default change event doesn't trigger on deep changes, so we'll trigger our own
+ this.trigger('change');
+ this.trigger('change:data');
+ }
+ },
+
+ /**
+ * Empty all the rows and the cells/widgets they contain.
+ */
+ emptyRows: function(){
+ _.invoke(this.rows.toArray(), 'destroy');
+ this.rows.reset();
+
+ return this;
+ }
+
+ } );
+
+ /**
+ * This is the main view for the Page Builder interface.
+ */
+ panels.view.builder = Backbone.View.extend( {
+ template: _.template( $('#siteorigin-panels-builder').html() ),
+ dialogs: { },
+ rowsSortable: null,
+ dataField : false,
+ currentData: '',
+
+ attachedToEditor: false,
+ liveEditor: false,
+
+ events: {
+ 'click .so-tool-button.so-widget-add': 'displayAddWidgetDialog',
+ 'click .so-tool-button.so-row-add': 'displayAddRowDialog',
+ 'click .so-tool-button.so-prebuilt-add': 'displayAddPrebuiltDialog',
+ 'click .so-tool-button.so-history': 'displayHistoryDialog',
+ 'click .so-tool-button.so-live-editor': 'displayLiveEditor',
+
+ 'click .so-cells .cell .cell-wrapper' : 'cellClickHandler'
+ },
+
+ /* A row collection */
+ rows: null,
+
+ /**
+ * Initialize the builder
+ */
+ initialize: function(){
+ var builder = this;
+
+ // Now lets create all the dialog boxes that the main builder interface uses
+ this.dialogs = {
+ widgets: new panels.dialog.widgets(),
+ row: new panels.dialog.row(),
+ prebuilt: new panels.dialog.prebuilt()
+ };
+
+ // Set the builder for each dialog and render it.
+ _.each(this.dialogs, function(p, i, d){
+ d[i].setBuilder( builder );
+ })
+
+ this.dialogs.row.setRowDialogType('create');
+
+ // This handles a new row being added to the collection - we'll display it in the interface
+ this.model.rows.on('add', this.onAddRow, this);
+
+ // Reflow the entire builder when ever the
+ $(window).resize(function(e){
+ if(e.target == window) {
+ builder.trigger('builder_resize');
+ }
+ });
+
+ // When the data changes in the model, store it in the field
+ this.model.on('change:data', this.storeModelData, this);
+
+ // Handle a content change
+ this.on('content_change', this.handleContentChange, this);
+
+ this.on('display_builder', this.handleDisplayBuilder, this);
+
+ this.model.on('change:data', this.toggleWelcomeDisplay, this);
+ },
+
+ /**
+ * Render the builder interface.
+ *
+ * @return {siteoriginPanels.view.builder}
+ */
+ render: function(){
+ this.$el.html( this.template() );
+ this.$el
+ .attr( 'id', 'siteorigin-panels-builder-' + this.cid )
+ .addClass('so-builder-container');
+ return this;
+ },
+
+ /**
+ * Attach the builder to the given container
+ *
+ * @param container
+ * @returns {panels.view.builder}
+ */
+ attach: function(options) {
+
+ options = _.extend({ container: false, dialog: false }, options);
+
+ if( options.dialog ) {
+ // We're going to add this to a dialog
+ this.dialog = new panels.dialog.builder();
+ this.dialog.builder = this;
+ }
+ else {
+ // Attach this in the standard way
+ this.$el.appendTo( options.container );
+ this.metabox = options.container.closest('.postbox');
+ this.initSortable();
+ }
+
+ return this;
+ },
+
+ /**
+ * This will move the Page Builder Metabox into the editor
+ *
+ * @returns {panels.view.builder}
+ */
+ attachToEditor: function(){
+ if( typeof this.metabox == 'undefined' ) return this;
+
+ this.attachedToEditor = true;
+ var metabox = this.metabox;
+ var thisView = this;
+
+ // Handle switching between the page builder and other tabs
+ $( '#wp-content-wrap .wp-editor-tabs' )
+ .find( '.wp-switch-editor' )
+ .click(function (e) {
+ e.preventDefault();
+ $( '#wp-content-editor-container, #post-status-info' ).show();
+ metabox.hide();
+ $( '#wp-content-wrap' ).removeClass('panels-active');
+ $('#content-resize-handle' ).show();
+ thisView.trigger('hide_builder');
+ } ).end()
+ .prepend(
+ $( '' + metabox.find( 'h3.hndle span' ).html() + '' )
+ .click( function (e) {
+ // Switch to the Page Builder interface
+ e.preventDefault();
+
+ var $$ = $( this );
+
+ // Hide the standard content editor
+ $( '#wp-content-wrap, #post-status-info' ).hide();
+
+ // Show page builder and the inside div
+ metabox.show().find('> .inside').show();
+
+ // Triggers full refresh
+ $( window ).resize();
+ $( document).scroll();
+
+ thisView.trigger('display_builder');
+ } )
+ );
+
+ // Switch back to the standard editor
+ metabox.find('.so-switch-to-standard').click(function(e){
+ e.preventDefault();
+
+ // Switch back to the standard editor
+ $( '#wp-content-wrap, #post-status-info' ).show();
+ metabox.hide();
+ // Resize to trigger reflow of WordPress editor stuff
+ $( window ).resize();
+ }).show();
+
+ // Move the panels box into a tab of the content editor
+ metabox.insertAfter( '#wp-content-wrap').hide().addClass('attached-to-editor');
+
+ // Switch to the Page Builder interface as soon as we load the page if there are widgets
+ var data = this.model.get('data');
+ if( typeof data.widgets != 'undefined' && _.size(data.widgets) != 0 ) {
+ $('#content-panels.switch-panels').click();
+ }
+
+ // We will also make this sticky if its attached to an editor.
+ var stickToolbar = function(){
+ var toolbar = thisView.$('.so-builder-toolbar');
+ var newTop = $(window).scrollTop() - thisView.$el.offset().top;
+
+ if( $('#wpadminbar').css('position') == 'fixed' ) {
+ newTop += $('#wpadminbar').outerHeight();
+ }
+
+ // Make sure this falls in an acceptible range.
+ newTop = Math.max( newTop, 0 );
+ newTop = Math.min( newTop, thisView.$el.outerHeight() - toolbar.outerHeight() + 20 ); // 20px extra to account for padding.
+
+ // Position the toolbar
+ toolbar.css('top', newTop);
+ thisView.$el.css('padding-top', toolbar.outerHeight());
+ }
+ $( window ).resize( stickToolbar );
+ $( document ).scroll( stickToolbar );
+ stickToolbar();
+
+ return this;
+ },
+
+ /**
+ * Initialize the row sortables
+ */
+ initSortable: function(){
+ // Create the sortable for the rows
+ var $el = this.$el;
+ var builderView = this;
+
+ this.rowsSortable = this.$el.find('.so-rows-container').sortable( {
+ appendTo: '#wpwrap',
+ items: '.so-row-container',
+ handle: '.so-row-move',
+ tolerance: 'pointer',
+ scroll: false,
+ stop: function (e) {
+ builderView.addHistoryEntry('row_moved');
+
+ // Sort the rows collection after updating all the indexes.
+ builderView.sortCollections();
+ }
+ } );
+ },
+
+ /**
+ * Refresh the row sortable
+ */
+ refreshSortable: function(){
+ // Refresh the sortable to account for the new row
+ if(this.rowsSortable != null) {
+ this.rowsSortable.sortable('refresh');
+ }
+ },
+
+ /**
+ * Set the field that's used to store the data
+ * @param field
+ */
+ setDataField: function(field, options){
+ options = _.extend({
+ load: true
+ }, options);
+
+ this.dataField = field;
+ this.dataField.data('builder', this);
+
+ if( options.load && field.val() != '') {
+ var data;
+ try {
+ data = JSON.parse( this.dataField.val( ) );
+ }
+ catch(err) { data = '' }
+
+ this.model.loadPanelsData(data);
+ this.currentData = data;
+ this.toggleWelcomeDisplay();
+ }
+
+ return this;
+ },
+
+ /**
+ * Store the model data in the data field set in this.setDataField.
+ */
+ storeModelData: function(){
+ var data = JSON.stringify( this.model.get('data' ) );
+
+ if( $(this.dataField).val( ) != data ) {
+ // If the data is different, set it and trigger a content_change event
+ $(this.dataField).val( data );
+ this.trigger('content_change');
+ }
+ },
+
+ onAddRow: function(row, collection, options){
+ options = _.extend( {noAnimate: false}, options );
+ // Create a view for the row
+ var rowView = new panels.view.row( { model: row } );
+ rowView.builder = this;
+ rowView.render();
+
+ // Attach the row elements to this builder
+ if( typeof options.at == 'undefined' || collection.length <= 1 ) {
+ // Insert this at the end of the widgets container
+ rowView.$el.appendTo( this.$( '.so-rows-container' ) );
+ }
+ else {
+ // We need to insert this at a specific position
+ rowView.$el.insertAfter(
+ this.$('.so-rows-container .so-row-container').eq( options.at - 1 )
+ );
+ }
+
+ if(options.noAnimate === false) {
+ rowView.visualCreate();
+ }
+
+ this.refreshSortable();
+ rowView.resize();
+ },
+
+ displayAddWidgetDialog: function(){
+ this.dialogs.widgets.openDialog();
+ return false;
+ },
+
+ displayAddRowDialog: function(){
+ this.dialogs.row.openDialog();
+ this.dialogs.row.setRowModel(); // Set this to an empty row model
+ return false;
+ },
+
+ displayAddPrebuiltDialog: function(){
+ this.dialogs.prebuilt.openDialog();
+ return false;
+ },
+
+ displayHistoryDialog: function(){
+ this.dialogs.history.openDialog();
+ return false;
+ },
+
+ cellClickHandler: function(e){
+ var cells = this.$el.find('.so-cells .cell').removeClass('cell-selected');
+ $(e.target).parent().addClass('cell-selected');
+ },
+
+ /**
+ * Get the model for the currently active cell
+ */
+ getActiveCell: function(){
+ if( this.$('.so-cells .cell').length == 0 ) {
+ // Create a row with a single cell
+ this.model.addRow( [1], {noAnimate: true} );
+ }
+
+ var activeCell = this.$('.so-cells .cell.cell-selected');
+
+ if(!activeCell.length) {
+ activeCell = this.$('.so-cells .cell').first();
+ }
+
+ return activeCell.data('view').model;
+ },
+
+ /**
+ * Sort all widget and row collections based on their dom position
+ */
+ sortCollections: function(){
+ // Create an array that stores model indexes within the array
+ var indexes = {};
+
+ this.$('.so-rows-container .so-row-container').each(function(ri, el){
+ var $r = $(el);
+ indexes[ $r.data('view').model.cid ] = ri;
+
+ $r.find('.so-cells .cell').each(function(ci, el){
+ var $c = $(el);
+
+ $c.find('.so-widget').each(function(wi, el) {
+ var $w = $(el);
+ indexes[ $w.data('view').model.cid ] = wi;
+ })
+ });
+ });
+
+ // Sort everything
+ this.model.rows.models = this.model.rows.sortBy(function(model){
+ return indexes[model.cid];
+ });
+
+ this.model.rows.each(function(row){
+ row.cells.each(function(cell){
+ cell.widgets.models = cell.widgets.sortBy(function(widget){
+ return indexes[widget.cid];
+ });
+ })
+ });
+
+ // Update the builder model to reflect the newly ordered data.
+ this.model.refreshPanelsData();
+ },
+
+ /**
+ * Add a live editor
+ *
+ * @returns {panels.view.builder}
+ */
+ addLiveEditor: function(postId){
+ if( typeof panels.view.liveEditor == 'undefined' ) return this;
+
+ // Create the live editor and set the builder to this.
+ this.liveEditor = new panels.view.liveEditor();
+ this.liveEditor.setPostId(postId);
+
+ this.liveEditor.builder = this;
+
+ // Display the live editor button in the toolbar
+ if( this.liveEditor.hasPreviewUrl() ) {
+ this.$('.so-builder-toolbar .so-live-editor').show();
+ }
+
+ return this;
+ },
+
+ /**
+ * Show the current live editor
+ */
+ displayLiveEditor: function(){
+ if(typeof this.liveEditor == 'undefined') return false;
+
+ this.liveEditor.open();
+ return false;
+ },
+
+ /**
+ * Add the history browser.
+ *
+ * @return {panels.view.builder}
+ */
+ addHistoryBrowser: function(){
+ if(typeof panels.dialog.history == 'undefined') return this;
+
+ this.dialogs.history = new panels.dialog.history();
+ this.dialogs.history.builder = this;
+ this.dialogs.history.entries.builder = this.model;
+
+ // Set the revert entry
+ this.dialogs.history.setRevertEntry( this.model );
+
+ // Display the live editor button in the toolbar
+ this.$('.so-builder-toolbar .so-history').show();
+ },
+
+ /**
+ * Add an entry.
+ *
+ * @param text
+ * @param data
+ */
+ addHistoryEntry: function(text, data){
+ if(typeof data == 'undefined') data = null;
+
+ if( typeof this.dialogs.history != 'undefined' ) {
+ this.dialogs.history.entries.addEntry(text, data);
+ }
+ },
+
+ /**
+ * Handle a change of the content
+ */
+ handleContentChange: function(){
+
+ if(this.attachedToEditor) {
+ // We're going to create a copy of page builder content into the post content
+ $.post(
+ ajaxurl,
+ {
+ action: 'so_panels_builder_content',
+ panels_data: JSON.stringify( this.model.getPanelsData() ),
+ post_id : $('#post_ID').val()
+ },
+ function(content){
+
+ // Strip all the known layout divs
+ var t = $('').html( content );
+ t.find( 'div').each(function() {
+ var c = $(this).contents();
+ $(this).replaceWith(c);
+ });
+ content = t.html();
+
+ // Set the content of the editor
+ if( typeof tinyMCE == 'undefined' || tinyMCE.get("content") == null ) $('#content').val( content );
+ else tinyMCE.get("content").setContent(content);
+
+ // Trigger a focusout (mainly for Yoast SEO)
+ $('#content').focusout();
+ }
+ );
+ }
+
+ if( this.liveEditor !== false ) {
+ // Refresh the content of the builder
+ this.liveEditor.refreshPreview();
+ }
+ },
+
+ /**
+ * Handle displaying the builder
+ */
+ handleDisplayBuilder: function(){
+ var editorContent = '';
+ if ( typeof tinyMCE != 'undefined' ) editor = tinyMCE.get( 'content' );
+ if( editor != null && typeof( editor.getContent ) == "function" ) {
+ editorContent = editor.getContent();
+ }
+ else {
+ editorContent = $('textarea#content').val();
+ }
+
+ if( this.model.get('data') == '' && editorContent != '') {
+ // Confirm with the user first
+ if( !confirm( panelsOptions.loc.confirm_use_builder ) ) return;
+
+ var widgetClass = '';
+ if( typeof panelsOptions.widgets["WP_Widget_Black_Studio_TinyMCE"] ) {
+ widgetClass = 'WP_Widget_Black_Studio_TinyMCE';
+ }
+ // There is a small chance a theme will have removed this, so check
+ else if( typeof panelsOptions.widgets["WP_Widget_Text"] ) {
+ widgetClass = 'WP_Widget_Text';
+ }
+
+ if( widgetClass == '' ) return;
+
+ // Create the existing page content in a single widget
+ this.model.loadPanelsData( {
+ grid_cells : [ { grid: 0, weight: 1 } ],
+ grids: [ { cells: 1 } ],
+ widgets: [{
+ filter: "1",
+ text: editorContent,
+ title: "",
+ type: "visual",
+ panels_info: {
+ class: widgetClass,
+ raw: false,
+ grid: 0,
+ cell: 0
+ }
+ }]
+ } );
+ this.model.trigger('change');
+ this.model.trigger('change:data');
+ }
+ else if ( this.model.get('data') == '' ) {
+ // Set up a blank single row
+ //this.model.loadPanelsData( {
+ // grid_cells : [ { grid: 0, weight: 1 } ],
+ // grids: [ { cells: 1 } ],
+ // widgets: []
+ //} );
+ }
+
+ },
+
+ /**
+ * Set the parent dialog for all the dialogs in this builder.
+ *
+ * @param text
+ * @param dialog
+ */
+ setDialogParents: function(text, dialog){
+ _.each(this.dialogs, function(p, i, d){
+ d[i].setParent(text, dialog );
+ });
+
+ // For any future dialogs
+ this.on('add_dialog', function(newDialog){
+ newDialog.setParent(text, dialog);
+ }, this)
+ },
+
+ toggleWelcomeDisplay: function(){
+ if( this.model.rows.length ) {
+ this.$('.so-panels-welcome-message').hide();
+ }
+ else {
+ this.$('.so-panels-welcome-message').show();
+ }
+ },
+
+ } );
+
+ /**
+ * The default dialog view. This should be extended by the other views.
+ */
+ panels.view.dialog = Backbone.View.extend( {
+ dialogTemplate: _.template( $('#siteorigin-panels-dialog').html() ),
+ dialogTabTemplate: _.template( $('#siteorigin-panels-dialog-tab').html() ),
+
+ tabbed: false,
+ rendered: false,
+ builder: false,
+ className: 'so-panels-dialog-wrapper',
+ dialogClass: '',
+ parentDialog: false,
+
+ events : {
+ 'click .so-close': 'closeDialog',
+ 'click .so-nav.so-previous': 'navToPrevious',
+ 'click .so-nav.so-next': 'navToNext'
+ },
+
+ initialize: function(){
+ // The first time this dialog is opened, render it
+ this.once('open_dialog', this.render);
+ this.once('open_dialog', this.attach);
+ this.once('open_dialog', this.setDialogClass);
+
+ this.trigger('initialize_dialog', this);
+
+ if(typeof this.initializeDialog != 'undefined') {
+ this.initializeDialog();
+ }
+ },
+
+ /**
+ * Returns the next dialog in the sequence. Should be overwritten by a child dialog.
+ * @returns {null}
+ */
+ getNextDialog: function(){
+ return null;
+ },
+
+ /**
+ * Returns the previous dialog in this sequence. Should be overwritten by child dialog.
+ * @returns {null}
+ */
+ getPrevDialog: function(){
+ return null;
+ },
+
+ /**
+ * Adds a dialog class to uniquely identify this dialog type
+ */
+ setDialogClass: function(){
+ if(this.dialogClass != ''){
+ this.$('.so-panels-dialog').addClass(this.dialogClass);
+ }
+ },
+
+ /**
+ * Set the builder that controls this dialog.
+ * @param {panels.view.builder} builder
+ */
+ setBuilder: function(builder){
+ this.builder = builder;
+
+ // Trigger an add dialog event on the builder so it can modify the dialog in any way
+ builder.trigger('add_dialog', this, this.builder);
+
+ return this;
+ },
+
+ /**
+ * Attach the dialog to the window
+ */
+ attach: function(){
+ this.$el.appendTo( 'body' );
+
+ return this;
+ },
+
+ /**
+ * Converts an HTML representation of the dialog into arguments for a dialog box
+ * @param html HTML for the dialog
+ * @param args Arguments passed to the template
+ * @returns {}
+ */
+ parseDialogContent: function(html, args){
+ // Add a CID
+ args = _.extend({cid: this.cid}, args);
+
+
+ var c = $( ( _.template(html) )( args ) );
+ var r = {
+ title : c.find('.title').html(),
+ buttons : c.find('.buttons').html(),
+ content : c.find('.content').html()
+ };
+
+ if( c.has('.left-sidebar') ){
+ r.left_sidebar = c.find('.left-sidebar').html();
+ }
+
+ if( c.has('.right-sidebar') ){
+ r.right_sidebar = c.find('.right-sidebar').html();
+ }
+
+ return r;
+
+ },
+
+ /**
+ * Render the dialog and initialize the tabs
+ *
+ * @param attributes
+ * @returns {panels.view.dialog}
+ */
+ renderDialog: function(attributes){
+ this.$el.html( this.dialogTemplate( attributes ) ).hide();
+ this.$el.data('view', this);
+ this.$el.addClass('so-panels-dialog-wrapper');
+
+ if( this.parentDialog != false ) {
+ // Add a link to the parent dialog as a sort of crumbtrail.
+ var thisDialog = this;
+ var dialogParent = $('').html( this.parentDialog.text + '' );
+ dialogParent.click(function(e){
+ e.preventDefault();
+ thisDialog.closeDialog();
+ thisDialog.parentDialog.openDialog();
+ });
+ this.$('.so-title-bar').prepend( dialogParent );
+ }
+
+ return this;
+ },
+
+ /**
+ * Initialize the sidebar tabs
+ */
+ initTabs: function(){
+ var tabs = this.$el.find('.so-sidebar-tabs li a');
+
+ if(tabs.length == 0) return;
+
+ var thisDialog = this;
+ tabs.click(function(e){
+ e.preventDefault();
+ var $$ = $(this);
+
+ thisDialog.$('.so-sidebar-tabs li').removeClass('tab-active');
+ thisDialog.$('.so-content .so-content-tabs > *').hide();
+
+ $$.parent().addClass('tab-active');
+
+ var url = $$.attr('href');
+ if(typeof url != 'undefined' && url.charAt(0) == '#') {
+ // Display the new tab
+ var tabName = url.split('#')[1];
+ thisDialog.$('.so-content .so-content-tabs .tab-' + tabName).show();
+ }
+
+ // This lets other dialogs implement their own custom handlers
+ thisDialog.trigger('tab_click', $$);
+
+ });
+
+ // Trigger a click on the first tab
+ this.$el.find('.so-sidebar-tabs li a').first().click();
+
+ },
+
+ /**
+ * Quickly setup the dialog by opening and closing it.
+ */
+ setupDialog: function(){
+ this.openDialog();
+ this.closeDialog();
+ },
+
+ /**
+ * Refresh the next and previous buttons.
+ */
+ refreshDialogNav: function(){
+ this.$('.so-title-bar .so-nav').show().removeClass('so-disabled');
+
+ // Lets also hide the next and previous if we don't have a next and previous dialog
+ var nextDialog = this.getNextDialog();
+ var nextButton = this.$('.so-title-bar .so-next');
+
+ var prevDialog = this.getPrevDialog();
+ var prevButton = this.$('.so-title-bar .so-previous');
+
+ if(nextDialog === null) nextButton.hide();
+ else if(nextDialog === false) nextButton.addClass('so-disabled');
+
+ if(prevDialog === null) prevButton.hide();
+ else if(prevDialog === false) prevButton.addClass('so-disabled');
+ },
+
+ /**
+ * Open the dialog
+ */
+ openDialog: function(){
+ this.trigger('open_dialog');
+
+ this.refreshDialogNav();
+
+ // Stop scrolling for the main body
+ this.bodyScrollTop = $('body').scrollTop();
+ $('body').css({'overflow':'hidden'});
+
+ this.$el.show();
+
+ // This triggers once everything is visible
+ this.trigger('open_dialog_complete');
+ },
+
+ /**
+ * Close the dialog
+ *
+ * @param e
+ * @returns {boolean}
+ */
+ closeDialog: function(e){
+ this.trigger('close_dialog');
+
+ // In the builder, trigger an update
+ if(typeof this.builder != 'undefined') {
+ // Store the model data when a dialog is closed.
+ this.builder.model.refreshPanelsData();
+ }
+
+ this.$el.hide();
+
+ if( !$('.so-panels-dialog-wrapper').is(':visible') ){
+ // Restore scrolling to the main body if there are no more dialogs
+ $('body').css({'overflow':'auto'});
+ $('body').scrollTop( this.bodyScrollTop );
+ }
+
+ // This triggers once everything is hidden
+ this.trigger('close_dialog_complete');
+
+ return false;
+ },
+
+ /**
+ * Navigate to the previous dialog
+ */
+ navToPrevious: function(){
+ this.closeDialog(null);
+
+ var prev = this.getPrevDialog();
+ if(prev != null && prev != false){
+ prev.openDialog();
+ }
+ },
+
+ /**
+ * Navigate to the next dialog
+ */
+ navToNext: function(){
+ this.closeDialog(null);
+
+ var next = this.getNextDialog();
+ if(next != null && next != false){
+ next.openDialog();
+ }
+ },
+
+ /**
+ * Get the values from the form and convert them into a data array
+ */
+ getFormValues: function(formSelector){
+ if(typeof formSelector == 'undefined') formSelector = '.so-content';
+ var $f = this.$(formSelector);
+
+ var data = {}, parts;
+
+ // Find all the named fields in the form
+ $f.find('[name]').each(function(){
+ var $$ = $(this);
+
+ var name = /([A-Za-z_]+)\[(.*)\]/.exec( $$.attr('name') );
+
+ // Create an array with the parts of the name
+ if(typeof name[2] == 'undefined') {
+ parts = $$.attr('name');
+ }
+ else {
+ parts = name[2].split('][');
+ parts.unshift( name[1] );
+ }
+
+ parts = parts.map(function(e){
+ if( !isNaN(parseFloat(e)) && isFinite(e) ) return parseInt(e);
+ else return e;
+ });
+
+ var sub = data;
+ var fieldValue = null;
+
+ // First we need to get the value from the field
+ if( $$.attr('type') == 'checkbox' ){
+ if ( $$.is(':checked') ) {
+ fieldValue = $$.val() != '' ? $$.val() : true;
+ }
+ else {
+ fieldValue = false;
+ }
+ }
+ else if( $$.attr('type') == 'radio' ){
+ if ( $$.is(':checked') ) {
+ fieldValue = $$.val();
+ }
+ else {
+ //skip over unchecked radios
+ return;
+ }
+ }
+ else if( $$.prop('tagName') == 'TEXTAREA' && $$.hasClass('wp-editor-area') ){
+ // This is a TinyMCE editor, so we'll use the tinyMCE object to get the content
+ var editor = null;
+ if ( typeof tinyMCE != 'undefined' ) editor = tinyMCE.get( $$.attr('id') );
+
+ if( editor != null && typeof( editor.getContent ) == "function" ) {
+ fieldValue = editor.getContent();
+ }
+ }
+ else if ( $$.prop('tagName') == 'SELECT' ) {
+ fieldValue = $$.find('option:selected').val();
+ }
+ if( fieldValue == null ) {
+ fieldValue = $$.val();
+ }
+
+ // Now, we need to filter this value if necessary
+ if( typeof $$.data('panels-filter') != 'undefined' ) {
+ switch( $$.data('panels-filter') ) {
+ case 'json_parse':
+ // Attempt to parse the JSON value of this field
+ try {
+ fieldValue = JSON.parse( fieldValue );
+ }
+ catch(err) {
+ fieldValue = '';
+ }
+ break;
+ }
+ }
+
+ // Now convert this into an array
+ for(var i = 0; i < parts.length; i++) {
+ if(i == parts.length - 1) {
+ sub[parts[i]] = fieldValue;
+ }
+ else {
+ if(typeof sub[parts[i]] == 'undefined') {
+ sub[parts[i]] = {};
+ }
+ sub = sub[parts[i]];
+ }
+ }
+
+ }); // End of each through input fields
+
+ return data;
+ },
+
+ /**
+ * Set a status message for the dialog
+ */
+ setStatusMessage: function(message, loading){
+ this.$('.so-toolbar .so-status').html( message );
+ if( typeof loading != 'undefined' && loading ) {
+ this.$('.so-toolbar .so-status').addClass('so-panels-loading');
+ }
+ },
+
+ /**
+ * Set the parent after.
+ */
+ setParent: function(text, dialog){
+ this.parentDialog = {
+ text: text,
+ dialog: dialog
+ }
+ }
+ } );
+
+ /**
+ * This is the dialog that holds the builder.
+ */
+ panels.dialog.builder = panels.view.dialog.extend( {
+ dialogClass : 'so-panels-dialog-add-builder',
+
+ render: function(){
+ // Render the dialog and attach it to the builder interface
+ this.renderDialog( this.parseDialogContent( $('#siteorigin-panels-dialog-builder').html(), {} ) );
+ this.$('.so-content .siteorigin-panels-builder').append( this.builder.$el );
+ },
+
+ initializeDialog: function(){
+ var thisView = this;
+ this.once('open_dialog_complete', function(){
+ thisView.builder.initSortable();
+ });
+
+ this.on('open_dialog_complete', function(){
+ thisView.builder.trigger('builder_resize');
+ });
+ }
+ } );
+
+ /**
+ * The dialog for selecting a widget to add to the page
+ */
+ panels.dialog.widgets = panels.view.dialog.extend( {
+
+ builder: null,
+ widgetTemplate: _.template( $('#siteorigin-panels-dialog-widgets-widget').html() ),
+ filter: {},
+
+ dialogClass : 'so-panels-dialog-add-widget',
+
+ events: {
+ 'click .so-close': 'closeDialog',
+ 'click .widget-type' : 'widgetClickHandler',
+ 'keyup .so-sidebar-search' : 'searchHandler'
+ },
+
+ /**
+ * Initialize the widget adding dialog
+ */
+ initializeDialog: function(){
+
+ this.on('open_dialog', function(){
+ this.filter.search = '';
+ this.filterWidgets(this.filter);
+ }, this);
+
+ this.on('open_dialog_complete', function(){
+ // Clear the search and re-filter the widgets when we open the dialog
+ this.$('.so-sidebar-search').val('').focus();
+ });
+
+ // We'll implement a custom tab click handler
+ this.on('tab_click', this.tabClickHandler, this);
+ },
+
+ render: function(){
+ // Render the dialog and attach it to the builder interface
+ this.renderDialog( this.parseDialogContent( $('#siteorigin-panels-dialog-widgets').html(), {} ) );
+
+ // Add all the widgets
+ _.each( panelsOptions.widgets, function( widget ){
+ var $w = $( this.widgetTemplate( {
+ title : widget.title,
+ description : widget.description
+ } ) ) ;
+
+ if(typeof widget.icon == 'undefined') widget.icon = 'dashicons dashicons-admin-generic';
+
+ if( typeof widget.icon != 'undefined' ){
+ $('').addClass( widget.icon ).prependTo( $w.find('.widget-type-wrapper') );
+ }
+
+ $w.data('class', widget.class).appendTo( this.$el.find('.widget-type-list') );
+ }, this );
+
+ // Add the sidebar tabs
+ var tabs = this.$el.find('.so-sidebar-tabs');
+ _.each(panelsOptions.widget_dialog_tabs, function(tab){
+ var $t = $( this.dialogTabTemplate( { 'title' : tab.title } )).data('filter', tab.filter).appendTo( tabs );
+ }, this);
+
+ // We'll be using tabs, so initialize them
+ this.initTabs();
+ },
+
+ /**
+ * Handle a tab being clicked
+ */
+ tabClickHandler: function($t){
+ // Get the filter from the tab, and filter the widgets
+ this.filter = $t.parent().data('filter');
+ if( this.$el.find('.so-sidebar-search').val() != '' ) {
+ this.filter.search = this.$el.find('.so-sidebar-search').val();
+ }
+ this.filterWidgets(this.filter);
+
+ return false;
+ },
+
+ /**
+ * Handle changes to the search value
+ */
+ searchHandler: function(e){
+ this.filter.search = $(e.target).val();
+ this.filterWidgets(this.filter);
+ },
+
+ /**
+ * Filter the widgets that we're displaying
+ * @param filter
+ */
+ filterWidgets: function(filter) {
+ if (typeof filter == 'undefined') filter = {};
+
+ if(typeof filter.groups == 'undefined') filter.groups = '';
+
+ this.$el.find('.widget-type-list .widget-type').each(function(){
+ var $$ = $(this), showWidget;
+ var widgetClass = $$.data('class');
+
+ var widgetData = ( typeof panelsOptions.widgets[widgetClass] != 'undefined' ) ? panelsOptions.widgets[widgetClass] : false;
+
+ if( filter.groups.length == 0 ) {
+ // This filter doesn't specify groups, so show all
+ showWidget = true;
+ }
+ else if( widgetData !== false && _.intersection(filter.groups, panelsOptions.widgets[widgetClass].groups).length ) {
+ // This widget is in the filter group
+ showWidget = true;
+ }
+ else {
+ // This widget is not in the filter group
+ showWidget = false;
+ }
+
+ // This can probably be done with a more intelligent operator
+ if( showWidget ) {
+
+ if( typeof filter.search != 'undefined' && filter.search != '' ) {
+ // Check if the widget title contains the search term
+ if( widgetData.title.toLowerCase().indexOf( filter.search.toLowerCase() ) == -1 ) {
+ showWidget = false;
+ }
+ }
+
+ }
+
+ if(showWidget) $$.show();
+ else $$.hide();
+ });
+ },
+
+ /**
+ * Add the widget to the current builder
+ *
+ * @param e
+ */
+ widgetClickHandler : function(e){
+ // Add the history entry
+ this.builder.addHistoryEntry('widget_added');
+
+ var $w = $(e.currentTarget);
+
+ var widget = new panels.model.widget( {
+ class: $w.data('class')
+ } );
+
+ // Add the widget to the cell model
+ widget.cell = this.builder.getActiveCell();
+ widget.cell.widgets.add( widget );
+
+ this.closeDialog();
+ }
+ } );
+
+ /**
+ * Dialog for displaying a single widget form
+ */
+ panels.dialog.widget = panels.view.dialog.extend( {
+
+ builder: null,
+ sidebarWidgetTemplate: _.template( $('#siteorigin-panels-dialog-widget-sidebar-widget').html() ),
+ dialogClass : 'so-panels-dialog-edit-widget',
+ widgetView : false,
+
+ events: {
+ 'click .so-close': 'saveHistory',
+ 'click .so-nav.so-previous': 'navToPrevious',
+ 'click .so-nav.so-next': 'navToNext',
+
+ // Action handlers
+ 'click .so-toolbar .so-delete': 'deleteHandler',
+ 'click .so-toolbar .so-duplicate': 'duplicateHandler'
+ },
+
+ initializeDialog: function(){
+ this.model.on('destroy', this.remove, this);
+ },
+
+ /**
+ * Render the widget dialog.
+ */
+ render: function() {
+ // Render the dialog and attach it to the builder interface
+ this.renderDialog( this.parseDialogContent( $('#siteorigin-panels-dialog-widget').html(), {} ) );
+ this.loadForm();
+
+ if( typeof panelsOptions.widgets[ this.model.get('class') ] != 'undefined') {
+ this.$('.so-title .widget-name').html( panelsOptions.widgets[ this.model.get('class')].title );
+ }
+ else {
+ this.$('.so-title .widget-name').html( panelsOptions.loc.missing_widget.title );
+ }
+
+ // Now we need to attach the style window
+ this.styles = new panels.view.styles();
+ this.styles.model = this.model;
+ this.styles.render( 'widget' );
+ this.styles.attach( this.$('.so-sidebar.so-right-sidebar') );
+
+ // Handle the loading class
+ this.styles.on('styles_loaded', function(){
+ this.$('.so-sidebar.so-right-sidebar').removeClass('so-panels-loading');
+ }, this);
+ this.$('.so-sidebar.so-right-sidebar').addClass('so-panels-loading');
+ },
+
+ /**
+ * Get the previous widget editing dialog by looking at the dom.
+ * @returns {*}
+ */
+ getPrevDialog: function(){
+ var widgets = this.builder.$('.so-cells .cell .so-widget');
+ if(widgets.length <= 1) return false;
+ var currentIndex = widgets.index( this.widgetView.$el );
+
+ if( currentIndex == 0 ) {
+ return false;
+ }
+ else {
+ var widgetView = widgets.eq(currentIndex - 1).data('view');
+ if(typeof widgetView == 'undefined') return false;
+
+ return widgetView.getEditDialog();
+ }
+ },
+
+ /**
+ * Get the next widget editing dialog by looking at the dom.
+ * @returns {*}
+ */
+ getNextDialog: function(){
+ var widgets = this.builder.$('.so-cells .cell .so-widget');
+ if(widgets.length <= 1) return false;
+ var currentIndex = widgets.index( this.widgetView.$el );
+
+ if( currentIndex == widgets.length - 1 ) {
+ return false;
+ }
+ else {
+ var widgetView = widgets.eq(currentIndex + 1).data('view');
+ if(typeof widgetView == 'undefined') return false;
+
+ return widgetView.getEditDialog();
+ }
+ },
+
+ /**
+ * Load the widget form from the server
+ */
+ loadForm: function(){
+ var thisView = this;
+ this.$el.find('.so-content').addClass('so-panels-loading');
+
+ var data = {
+ 'action' : 'so_panels_widget_form',
+ 'widget' : this.model.get('class'),
+ 'instance' : JSON.stringify( this.model.get('values') ),
+ 'raw' : this.model.get('raw')
+ };
+
+ $.post(
+ ajaxurl,
+ data,
+ function(result){
+ // Add in the CID of the widget model
+ var html = result.replace( /\{\$id\}/g, thisView.model.cid );
+
+ // Load this content into the form
+ thisView.$el.find('.so-content')
+ .removeClass('so-panels-loading')
+ .html(html);
+
+ // Trigger all the necessary events
+ thisView.trigger('form_loaded', thisView);
+
+ // For legacy compatibility, trigger a panelsopen event
+ thisView.$el.find('.panel-dialog').trigger('panelsopen');
+
+ // If the main dialog is closed from this point on, save the widget content
+ thisView.on('close_dialog', thisView.saveWidget, thisView);
+ },
+ 'html'
+ );
+ },
+
+ /**
+ * Save the widget from the form to the model
+ */
+ saveWidget: function(){
+ // Get the values from the form and assign the new values to the model
+ var values = this.getFormValues();
+ if(typeof values.widgets == 'undefined') return;
+ values = values.widgets;
+ values = values[Object.keys(values)[0]];
+
+ this.model.setValues(values);
+ this.model.set('raw', true); // We've saved from the widget form, so this is now raw
+
+ if( this.styles.stylesLoaded ) {
+ // If the styles view has loaded
+ var style = {};
+ try {
+ var style = this.getFormValues('.so-sidebar .so-visual-styles').style;
+ }
+ catch (e) {
+ }
+ this.model.set('style', style);
+ }
+ },
+
+ saveHistory: function(){
+ this.builder.addHistoryEntry('widget_edited');
+ this.closeDialog();
+ },
+
+ deleteHandler: function(){
+
+ if(this.builder.liveEditor.displayed) {
+ // We need to instantly destroy the widget
+ this.model.destroy();
+ this.builder.liveEditor.refreshWidgets();
+ }
+ else {
+ this.model.trigger('visual_destroy');
+ }
+
+ this.closeDialog();
+
+ return false;
+ },
+
+ duplicateHandler: function(){
+ this.model.trigger('user_duplicate');
+
+ if(this.builder.liveEditor.displayed) {
+ this.builder.liveEditor.refreshWidgets();
+ }
+
+ this.closeDialog();
+
+ return false;
+ }
+
+ } );
+
+ /**
+ * The dialog box for displaying prebuilt layouts.
+ */
+ panels.dialog.prebuilt = panels.view.dialog.extend( {
+
+ entryTemplate : _.template( $('#siteorigin-panels-dialog-prebuilt-entry').html() ),
+ builder: null,
+ dialogClass : 'so-panels-dialog-prebuilt-layouts',
+
+ layoutCache : {},
+ currentTab : false,
+
+ events: {
+ 'click .so-close': 'closeDialog',
+ 'click .so-sidebar-tabs li a' : 'tabClickHandler',
+ 'click .so-content .layout' : 'layoutClickHandler',
+ 'keyup .so-sidebar-search' : 'searchHandler'
+ },
+
+ /**
+ * Initialize the prebuilt dialog.
+ */
+ initializeDialog: function(){
+ var thisView = this;
+
+ this.on('open_dialog', function(){
+ thisView.$('.so-sidebar-tabs li a[href="#prebuilt"]').click();
+ });
+ },
+
+ /**
+ * Render the prebuilt layouts dialog
+ */
+ render: function(){
+ this.renderDialog( this.parseDialogContent( $('#siteorigin-panels-dialog-prebuilt').html(), {} ) );
+ },
+
+ /**
+ *
+ * @param e
+ * @return {boolean}
+ */
+ tabClickHandler: function(e){
+ this.$('.so-sidebar-tabs li').removeClass('tab-active');
+
+ var $$ = $(e.target);
+ var tab = $$.attr('href').split('#')[1];
+ $$.parent().addClass( 'tab-active' );
+
+ var thisView = this;
+
+ // Empty everything
+ this.$('.so-content').empty();
+
+ this.currentTab = tab;
+
+ if( typeof this.layoutCache[tab] == 'undefined' ) {
+ // We need to load the tab items from the server
+ this.$('.so-content').addClass('so-panels-loading');
+
+ $.post(
+ ajaxurl,
+ {
+ action: 'so_panels_prebuilt_layouts',
+ type: tab
+ },
+ function(layouts){
+ thisView.layoutCache[ tab ] = layouts;
+ thisView.$( '.so-content' ).removeClass( 'so-panels-loading' );
+ thisView.displayLayouts( tab, layouts );
+ }
+ );
+ }
+ else {
+ thisView.displayLayouts(tab, this.layoutCache[tab]);
+ }
+
+ return false;
+ },
+
+ /**
+ * Display a list of layouts taking into account the search argument
+ */
+ displayLayouts: function(type, layouts){
+ var c = this.$('.so-content').empty();
+ var query = this.$('.so-sidebar-search').val().toLowerCase();
+
+ if( typeof layouts.error_message != 'undefined' ) {
+ this.$('.so-content').append(
+ $('
').html( layouts.error_message )
+ );
+ return;
+ }
+
+ for(var lid in layouts) {
+ // Exclude the current post if we have one
+ if( type != 'prebuilt' && lid == $('#post_ID').val() ) continue;
+ if(query != '' && layouts[lid].name.toLowerCase().indexOf( query ) === -1 ) continue;
+
+ var $l = $( this.entryTemplate( {
+ name: layouts[lid].name,
+ description: layouts[lid].description
+ } ) );
+
+ // Create and append the
+ $l.appendTo(c).data({'type' : type, 'lid' : lid});
+ }
+ },
+
+ /**
+ * Make the layout selected.
+ * @param e
+ */
+ layoutClickHandler: function(e){
+ var layout = $(e.target).closest('.layout');
+
+ this.loadLayout(
+ layout.data('type'),
+ layout.data('lid')
+ );
+
+ return false;
+ },
+
+ /**
+ * Load the layout into the main builder
+ */
+ loadLayout: function(type, lid){
+ var thisView = this;
+
+ if( !confirm(panelsOptions.loc.prebuilt_confirm) ) return false;
+ this.setStatusMessage(panelsOptions.loc.prebuilt_loading, true);
+
+ $.post(
+ ajaxurl,
+ {
+ action: 'so_panels_get_prebuilt_layout',
+ type: type,
+ lid: lid
+ },
+ function(layout){
+ // TODO check for an error message
+ thisView.setStatusMessage('', false);
+ thisView.builder.addHistoryEntry('prebuilt_loaded');
+
+ console.log(layout);
+
+ thisView.builder.model.loadPanelsData(layout);
+ thisView.closeDialog();
+
+ }
+ );
+ },
+
+ /**
+ * Handle an update to the search
+ */
+ searchHandler: function(){
+ if( this.currentTab == false || typeof this.layoutCache[ this.currentTab ] == 'undefined') return;
+ this.displayLayouts(this.currentTab, this.layoutCache[ this.currentTab ] );
+ }
+
+ } );
+
+ /**
+ * The dialog for adding and editing a row
+ */
+ panels.dialog.row = panels.view.dialog.extend( {
+
+ cellPreviewTemplate : _.template( $('#siteorigin-panels-dialog-row-cell-preview').html() ),
+
+ events: {
+ 'click .so-close': 'closeDialog',
+
+ // Toolbar buttons
+ 'click .so-toolbar .so-save': 'saveHandler',
+ 'click .so-toolbar .so-insert': 'insertHandler',
+ 'click .so-toolbar .so-delete': 'deleteHandler',
+ 'click .so-toolbar .so-duplicate': 'duplicateHandler',
+
+ // Changing the row
+ 'change .row-set-form > *': 'setCellsFromForm',
+ 'click .row-set-form button.set-row': 'setCellsFromForm'
+ },
+
+ dialogClass : 'so-panels-dialog-row-edit',
+ styleType : 'row',
+
+ /* This is used by */
+ dialogType : 'edit',
+
+ /**
+ * The current settings, not yet saved to the model
+ */
+ row : {
+ // This is just the cell weights, cell content is not edited by this dialog
+ cells : [ ],
+ // The style settings of the row
+ style : { }
+ },
+
+ initializeDialog: function(){
+ this.on('open_dialog', function(){
+ if( typeof this.model != 'undefined' && this.model.cells.length != 0 ) {
+ this.setRowModel( this.model );
+ }
+ else {
+ this.setRowModel( null );
+ }
+
+ this.regenerateRowPreview();
+ }, this);
+
+ // This is the default row layout
+ this.row = {
+ cells : [0.5, 0.5],
+ style : { }
+ }
+ },
+
+ /**
+ *
+ * @param dialogType Either "edit" or "create"
+ */
+ setRowDialogType: function(dialogType){
+ this.dialogType = dialogType;
+ },
+
+ /**
+ * Render the new row dialog
+ */
+ render: function(dialogType){
+ this.renderDialog( this.parseDialogContent( $('#siteorigin-panels-dialog-row').html(), { dialogType: this.dialogType } ) );
+
+ if( this.dialogType == 'edit' ) {
+ // Now we need to attach the style window
+ this.styles = new panels.view.styles();
+ this.styles.model = this.model;
+ this.styles.render( 'row' );
+ this.styles.attach( this.$('.so-sidebar.so-right-sidebar') );
+
+ // Handle the loading class
+ this.styles.on('styles_loaded', function(){
+ this.$('.so-sidebar.so-right-sidebar').removeClass('so-panels-loading');
+ }, this);
+ this.$('.so-sidebar.so-right-sidebar').addClass('so-panels-loading');
+ }
+
+ if( typeof this.model != 'undefined' ) {
+ // Set the initial value of the
+ this.$('input.so-row-field').val( this.model.cells.length );
+ }
+
+ var thisView = this;
+ this.$('input.so-row-field').keyup( function(){
+ $(this).trigger('change');
+ } );
+
+ return this;
+ },
+
+ /**
+ * Set the row model we'll be using for this dialog.
+ *
+ * @param model
+ */
+ setRowModel: function(model){
+ this.model = model;
+ if( this.model == null ) return;
+
+ // Set the rows to be a copy of the model
+ this.row = {
+ cells: this.model.cells.map( function(cell){
+ return cell.get('weight');
+ } ),
+ style: { }
+ }
+
+ // Set the initial value of the cell field.
+ this.$('input.so-row-field').val( this.model.cells.length );
+
+ return this;
+ },
+
+ /**
+ * Regenerate the row preview and resizing interface.
+ *
+ * @todo refactor this so we use the original row view.
+ */
+ regenerateRowPreview: function(){
+ var thisDialog = this;
+ var rowPreview = this.$('.row-preview');
+
+ rowPreview.empty();
+
+ var timeout;
+
+ // Represent the cells
+ _.each(this.row.cells, function(cell, i){
+ var newCell = $( this.cellPreviewTemplate( { weight: cell } ) );
+ rowPreview.append( newCell );
+
+ var prevCell = newCell.prev();
+ var handle;
+
+ if( prevCell.length != 0 ) {
+ handle = $('');
+ handle
+ .appendTo( newCell )
+ .dblclick(function(){
+ var t = thisDialog.row.cells[i] + thisDialog.row.cells[i-1];
+ thisDialog.row.cells[i] = thisDialog.row.cells[i-1] = t/2;
+ thisDialog.scaleRowWidths();
+ });
+
+ handle.draggable({
+ axis: 'x',
+ containment: rowPreview,
+ start: function(e, ui){
+
+ // Create the clone for the current cell
+ var newCellClone = newCell.clone().appendTo(ui.helper).css({
+ position : 'absolute',
+ top : '0',
+ width : newCell.outerWidth(),
+ left : 6,
+ height: newCell.outerHeight()
+ });
+ newCellClone.find('.resize-handle').remove();
+
+ // Create the clone for the previous cell
+ var prevCellClone = prevCell.clone().appendTo(ui.helper).css({
+ position : 'absolute',
+ top : '0',
+ width : prevCell.outerWidth(),
+ right : 6,
+ height: prevCell.outerHeight()
+ });
+ prevCellClone.find('.resize-handle').remove();
+
+ $(this).data({
+ 'newCellClone' : newCellClone,
+ 'prevCellClone' : prevCellClone
+ });
+
+ // Hide the
+ newCell.find('> .preview-cell-in').css('visibility', 'hidden');
+ prevCell.find('> .preview-cell-in').css('visibility', 'hidden');
+ },
+ drag: function(e, ui){
+ // Calculate the new cell and previous cell widths as a percent
+ var ncw = thisDialog.row.cells[i] - ( ( ui.position.left + 6 ) / rowPreview.width() );
+ var pcw = thisDialog.row.cells[i-1] + ( ( ui.position.left + 6 ) / rowPreview.width() );
+
+ var helperLeft = ui.helper.offset().left - rowPreview.offset().left - 6;
+
+ $(this).data('newCellClone').css('width', rowPreview.width() * ncw )
+ .find('.preview-cell-weight').html( Math.round(ncw*1000)/10 );
+
+ $(this).data('prevCellClone').css('width', rowPreview.width() * pcw )
+ .find('.preview-cell-weight').html( Math.round(pcw*1000)/10 );
+ },
+ stop: function(e, ui){
+ // Remove the clones
+ $(this).data('newCellClone').remove();
+ $(this).data('prevCellClone').remove();
+
+ // Reshow the main cells
+ newCell.find('.preview-cell-in').css('visibility', 'visible');
+ prevCell.find('.preview-cell-in').css('visibility', 'visible');
+
+ // Calculate the new cell weights
+ var offset = ui.position.left + 6;
+ var percent = offset / rowPreview.width();
+
+ // Ignore this if any of the cells are below 2% in width.
+ if( thisDialog.row.cells[i] - percent > 0.02 && thisDialog.row.cells[i-1] + percent > 0.02 ) {
+ thisDialog.row.cells[i] -= percent;
+ thisDialog.row.cells[i-1] += percent;
+ }
+
+ thisDialog.scaleRowWidths();
+ ui.helper.css('left', -6);
+ }
+ });
+ }
+
+ // Make this row weight click editable
+ newCell.find('.preview-cell-weight').click(function(ci){
+
+ // Disable the draggable while entering values
+ thisDialog.$('.resize-handle').css('pointer-event', 'none').draggable('disable');
+
+ rowPreview.find('.preview-cell-weight').each( function(){
+ var $$ = $(this).hide();
+ $('')
+ .val( parseFloat( $$.html() ) ).insertAfter( $$ )
+ .focus( function(){
+ clearTimeout( timeout );
+ } )
+ .keyup(function(e){
+ // Enter is clicked
+ if(e.keyCode == 13){
+ e.preventDefault();
+
+ $(this).removeClass('no-user-interacted');
+
+ // Select the next input
+ var inputs = rowPreview.find('.preview-cell-weight-input');
+ var index = inputs.index( $(this) );
+
+ if(index == inputs.length - 1) index = 0; // Go to first input
+ else index = index + 1; // Go to next
+
+ var next = rowPreview.find('.preview-cell-weight-input').eq( index );
+
+ // Either go to the next input or blur to set
+ if( !next.hasClass('no-user-interacted') ) $(this).blur();
+ else next.select();
+ }
+ })
+ .blur( function(){
+ timeout = setTimeout( function(){
+ // If there are no weight inputs, then skip this
+ if( rowPreview.find( '.preview-cell-weight-input').length == 0 ) return;
+
+ // Go through all the inputs
+ var rowWeights = [];
+ rowPreview.find( '.preview-cell-weight-input' ).each(function(i, el){
+ var val = parseFloat( $(el).val() );
+ if( val == NaN ) val = 1 / thisDialog.cells.length;
+ else val = val / 100;
+
+ rowWeights.push( val );
+ });
+
+ // Make sure the sum is 1
+ var sum = 0;
+ for( var j = 0; j < rowWeights.length; j++ ) {
+ sum += rowWeights[j];
+ }
+ for( var j = 0; j < rowWeights.length; j++ ) {
+ rowWeights[j] = rowWeights[j] / sum;
+ }
+
+ // Set the new cell weights and regenerate the preview.
+ if( Math.min.apply(Math, rowWeights) > 0.01 ) {
+ thisDialog.row.cells = rowWeights;
+ }
+
+ thisDialog.regenerateRowPreview();
+
+ }, 100 );
+ } )
+ .click( function(){
+ rowPreview.find('.preview-cell-weight-input').addClass('no-user-interacted');
+ $(this).select();
+ } );
+ } );
+
+ $(this).siblings('.preview-cell-weight-input').select();
+
+ });
+
+ }, this);
+ },
+
+ /**
+ * Visually scale the row widths based on the cell weights
+ */
+ scaleRowWidths: function(){
+ var thisDialog = this;
+ this.$('.row-preview .preview-cell').each(function(i, el){
+ $(el)
+ .css('width', thisDialog.row.cells[i] * 100 + "%")
+ .find('.preview-cell-weight').html( Math.round( thisDialog.row.cells[i] * 1000 )/10 )
+ });
+ },
+
+ /**
+ * Get the weights from the
+ */
+ setCellsFromForm: function(){
+ var f = {
+ 'cells' : parseInt( this.$el.find('.row-set-form input[name="cells"]').val() ),
+ 'ratio' : parseFloat( this.$el.find('.row-set-form select[name="ratio"]').val() ),
+ 'direction' : this.$el.find('.row-set-form select[name="ratio_direction"]').val()
+ }
+ var cells = [];
+
+ // Ignore this if the ratio or cell count is NaN
+ if( isNaN(f.cells) || isNaN(f.ratio) ) return;
+
+ if( f.cells < 1 ) {
+ this.$el.find('.row-set-form input[name="cells"]').val(1);
+ f.cells = 1;
+ }
+ else if (f.cells > 20) {
+ this.$el.find('.row-set-form input[name="cells"]').val(20);
+ f.cells = 20;
+ }
+
+ // Now, lets create some cells
+ var currentWeight = 1;
+ for( var i = 0; i < f.cells; i++ ) {
+ cells.push (currentWeight);
+ currentWeight *= f.ratio;
+ }
+
+ // Now lets make sure that the row weights add up to 1
+
+ var totalRowWeight = _.reduce( cells, function(memo, weight){ return memo + weight });
+ cells = _.map(cells, function(cell){
+ return cell/totalRowWeight;
+ });
+
+ // Don't return cells that are too small
+ cells = _.filter(cells, function(cell){ return cell > 0.01 });
+
+ if(f.direction == 'left') {
+ cells = cells.reverse();
+ }
+
+ this.row.cells = cells;
+ this.regenerateRowPreview();
+
+ // Remove the button primary class
+ this.$el.find('.row-set-form .so-button-row-set').removeClass('button-primary');
+ },
+
+ /**
+ * Handle a click on the dialog left bar tab
+ */
+ tabClickHandler : function($t){
+ if($t.attr('href') == '#row-layout') {
+ this.$('.so-panels-dialog').addClass('so-panels-dialog-has-right-sidebar');
+ }
+ else {
+ this.$('.so-panels-dialog').removeClass('so-panels-dialog-has-right-sidebar');
+ }
+ },
+
+ /**
+ * Update the current model with what we have in the dialog
+ */
+ updateModel: function(){
+ // Set the cells
+ this.model.setCells( this.row.cells );
+
+ // Update the styles if they've loaded
+ if ( typeof this.styles != 'undefined' && this.styles.stylesLoaded ) {
+ // This is an edit dialog, so there are styles
+ var style = {};
+ try {
+ var style = this.getFormValues('.so-sidebar .so-visual-styles').style;
+ }
+ catch( e ) { }
+
+ this.model.set('style', style);
+ }
+ },
+
+ /**
+ * Insert the new row
+ */
+ insertHandler: function(){
+ this.builder.addHistoryEntry('row_added');
+
+ this.model = new panels.model.row();
+ this.updateModel();
+
+ // Set up the model and add it to the builder
+ this.model.collection = this.builder.model.rows;
+ this.builder.model.rows.add( this.model );
+
+ this.closeDialog();
+
+ return false;
+ },
+
+ /**
+ * We'll just save this model and close the dialog
+ */
+ saveHandler: function(){
+ this.builder.addHistoryEntry('row_edited');
+ this.updateModel();
+ this.closeDialog();
+
+ return false;
+ },
+
+ /**
+ * The user clicks delete, so trigger deletion on the row model
+ */
+ deleteHandler: function(){
+ // Trigger a destroy on the model that will happen with a visual indication to the user
+ this.model.trigger('visual_destroy');
+ this.closeDialog();
+
+ return false;
+ },
+
+ /**
+ * Duplicate this row
+ */
+ duplicateHandler: function(){
+ this.builder.addHistoryEntry('row_duplicated');
+
+ var duplicateRow = this.model.clone( this.builder.model );
+
+ this.builder.model.rows.add( duplicateRow, {
+ at: this.builder.model.rows.indexOf( this.model ) + 1
+ } );
+
+ this.closeDialog();
+
+ return false;
+ }
+
+ } );
+
+ // Return the SiteOrigin Panels app
+ window.siteoriginPanels = panels;
+
+} )( jQuery, _, soPanelsOptions );
+
+// Set up Page Builder if we're on the main interface
+jQuery( function($){
+
+ var container = false, field = false, form = false, postId = false;
+
+ if( $('#siteorigin-panels-metabox').length && $('form#post').length ) {
+ container = $( '#siteorigin-panels-metabox' );
+ field = $( '#siteorigin-panels-metabox .siteorigin-panels-data-field' );
+ form = $('form#post');
+ postId = $('#post_ID').val();
+ }
+ else if( $('div#panels-home-page.wrap').length ) {
+ // We're dealing with the custom home page interface
+ var $$ = $('div#panels-home-page.wrap');
+ container = $$.find('.siteorigin-panels-builder');
+ field = $$.find('input[name="panels_data"]');
+ form = $$.find('form');
+ postId = $('#panels-home-page').data('post-id');
+ }
+
+ if( container != false ) {
+ // If we have a container, then set up the main builder
+ var panels = window.siteoriginPanels;
+
+ // Create the main builder model
+ var builderModel = new panels.model.builder();
+
+ // Now for the view to display the builder
+ var builderView = new panels.view.builder( {
+ model: builderModel
+ } );
+
+ // Set up the builder view
+ builderView
+ .render()
+ .attach( { container: container } )
+ .setDataField( field )
+ .attachToEditor()
+ .addLiveEditor( postId )
+ .addHistoryBrowser();
+
+ // When the form is submitted, update the panels data
+ form.submit( function(e){
+ // Refresh the data
+ builderModel.refreshPanelsData();
+ } );
+
+ container.removeClass('so-panels-loading');
+ }
+} );
+
+// A basic jQuery plugin for setting up a Page Builder widget.
+(function ( $ ) {
+
+ var panels = window.siteoriginPanels;
+
+ $.fn.soPanelsSetupBuilderWidget = function () {
+
+ return this.each(function(){
+ var $$ = $(this);
+ var widgetId = $$.closest('form').find('.widget-id').val();
+
+ // Exit if this isn't a real widget
+ if( widgetId != null && widgetId.indexOf('__i__') > -1 ) {
+ return;
+ }
+
+ // Create the main builder model
+ var builderModel = new panels.model.builder();
+
+ // Now for the view to display the builder
+ var builderView = new panels.view.builder( {
+ model: builderModel
+ } );
+
+ // Save panels data when we close the dialog, if we're in a dialog
+ var dialog = $$.closest('.so-panels-dialog-wrapper').data('view');
+ if( dialog != null ) {
+ dialog.on('close_dialog', function(){
+ builderModel.refreshPanelsData();
+ } );
+
+ dialog.on('open_dialog_complete', function(){
+ // Make sure the new layout widget is always properly setup
+ builderView.trigger('builder_resize');
+ });
+
+ dialog.model.on('destroy', function(){
+ // Destroy the builder
+ builderModel.emptyRows().destroy();
+ } );
+
+ // Set the parent for all the sub dialogs
+ builderView.setDialogParents(soPanelsOptions.loc.layout_widget, dialog);
+ }
+
+ // Basic setup for the builder
+ var isWidget = Boolean( $$.closest('.widget-content').length );
+ builderView
+ .render()
+ .attach( { container: $$, dialog: isWidget } )
+ .setDataField( $$.find('input.panels-data') );
+
+ if( isWidget ) {
+ // Set up the dialog opening
+ builderView.setDialogParents(soPanelsOptions.loc.layout_widget, builderView.dialog);
+ $$.find( '.siteorigin-panels-display-builder').click(function(){
+ builderView.dialog.openDialog();
+ });
+ }
+ else {
+ // Remove the dialog opener button, this is already being displayed in a page builder dialog.
+ $$.find( '.siteorigin-panels-display-builder').parent().remove();
+ }
+
+ });
+ };
+
+ // Setup new widgets when they're added in the standard widget interface
+ $(document).on('widget-added', function(e, widget) {
+ $(widget).find('.siteorigin-page-builder-widget').soPanelsSetupBuilderWidget();
+ });
+
+ // Setup existing widgets on the page (for the widgets interface)
+ $(function(){
+ $('.siteorigin-page-builder-widget').soPanelsSetupBuilderWidget();
+ });
+
+})(jQuery);
\ No newline at end of file
diff --git a/js/styling.js b/js/styling.js
new file mode 100644
index 000000000..a06370b98
--- /dev/null
+++ b/js/styling.js
@@ -0,0 +1,45 @@
+jQuery(function($){
+
+ // This will handle stretching the cells.
+ $('.siteorigin-panels-stretch.panel-row-style').each(function(){
+ var $$ = $(this);
+
+ var onResize = function(){
+
+ $$.css({
+ 'margin-left' : 0,
+ 'margin-right' : 0,
+ 'padding-left' : 0,
+ 'padding-right' : 0
+ });
+
+ var leftSpace = $$.offset().left;
+ var rightSpace = $(window).outerWidth() - $$.offset().left - $$.parent().outerWidth();
+
+ $$.css({
+ 'margin-left' : -leftSpace,
+ 'margin-right' : -rightSpace,
+ 'padding-left' : $$.data('stretch-type') == 'full' ? leftSpace : 0,
+ 'padding-right' : $$.data('stretch-type') == 'full' ? rightSpace : 0
+ });
+
+ var cells = $$.find('> .panel-grid-cell');
+
+ if( $$.data('stretch-type') == 'full-stretched' && cells.length == 1 ) {
+ cells.css({
+ 'padding-left' : 0,
+ 'padding-right' : 0
+ });
+ }
+ }
+
+ $(window).resize( onResize );
+ onResize();
+
+ $$.css({
+ 'border-left' : 0,
+ 'border-right' : 0
+ });
+ });
+
+});
\ No newline at end of file
diff --git a/lang/siteorigin-panels.po b/lang/siteorigin-panels.po
new file mode 100644
index 000000000..d6684e42a
--- /dev/null
+++ b/lang/siteorigin-panels.po
@@ -0,0 +1,827 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Page Builder by SiteOrigin\n"
+"POT-Creation-Date: 2014-09-15 22:17+0200\n"
+"PO-Revision-Date: 2014-09-15 22:17+0200\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.6.7\n"
+"X-Poedit-Basepath: ..\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;"
+"esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;"
+"_nx_noop:3c,1,2;__ngettext_noop:1,2\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: inc/legacy.php:22 siteorigin-panels.php:104
+msgid "Home"
+msgstr ""
+
+#: inc/notice.php:17
+#, php-format
+msgid ""
+"You've successfully installed Page Builder version %s. "
+msgstr ""
+
+#: inc/notice.php:20
+#, php-format
+msgid ""
+"You've successfully updated Page Builder to version %s. "
+msgstr ""
+
+#: inc/notice.php:24
+#, php-format
+msgid ""
+"Please post on our support forums if "
+"you have any issues and sign up to our "
+"newsletter to stay up to date."
+msgstr ""
+
+#: inc/notice.php:31 inc/notice.php:102
+msgid "Support Forums"
+msgstr ""
+
+#: inc/notice.php:32 siteorigin-panels.php:1170
+msgid "Newsletter"
+msgstr ""
+
+#: inc/notice.php:34 inc/notice.php:103
+msgid "Dismiss"
+msgstr ""
+
+#: inc/notice.php:84
+msgid ""
+"One or more of your active plugins are known to be incompatible with Page "
+"Builder."
+msgstr ""
+
+#: inc/notice.php:93
+msgid "More"
+msgstr ""
+
+#: inc/options.php:68 tpl/options.php:5
+msgid "SiteOrigin Page Builder"
+msgstr ""
+
+#: inc/options.php:107
+msgid "Post types that will have the page builder available"
+msgstr ""
+
+#: inc/options.php:122
+msgid "Enabled"
+msgstr ""
+
+#: inc/options.php:127
+msgid "px"
+msgstr ""
+
+#: inc/revisions.php:50
+msgid "Page Builder Content"
+msgstr ""
+
+#: inc/styles.php:19
+msgid "Class"
+msgstr ""
+
+#: inc/styles.php:22 widgets/basic.php:59 widgets/basic.php:482
+#: widgets/widgets.php:625
+msgid "Default"
+msgstr ""
+
+#: inc/styles.php:36
+msgid "Your theme doesn't provide any visual style fields. "
+msgstr ""
+
+#: siteorigin-panels.php:65
+msgid "Custom Home Page Builder"
+msgstr ""
+
+#: siteorigin-panels.php:66
+msgid "Home Page"
+msgstr ""
+
+#: siteorigin-panels.php:78 siteorigin-panels.php:87 siteorigin-panels.php:336
+msgid "Page Builder"
+msgstr ""
+
+#: siteorigin-panels.php:229
+msgid "Insert"
+msgstr ""
+
+#: siteorigin-panels.php:230
+msgid "cancel"
+msgstr ""
+
+#: siteorigin-panels.php:231
+msgid "Delete"
+msgstr ""
+
+#: siteorigin-panels.php:232
+msgid "Duplicate"
+msgstr ""
+
+#: siteorigin-panels.php:233
+msgid "Edit"
+msgstr ""
+
+#: siteorigin-panels.php:234
+msgid "Done"
+msgstr ""
+
+#: siteorigin-panels.php:235
+msgid "Undo"
+msgstr ""
+
+#: siteorigin-panels.php:236
+msgid "Add"
+msgstr ""
+
+#: siteorigin-panels.php:239
+msgid "Columns deleted"
+msgstr ""
+
+#: siteorigin-panels.php:240
+msgid "Widget deleted"
+msgstr ""
+
+#: siteorigin-panels.php:241
+msgid ""
+"Are you sure you want to load this layout? It will overwrite your current "
+"page."
+msgstr ""
+
+#: siteorigin-panels.php:242
+#, php-format
+msgid "Edit %s Widget"
+msgstr ""
+
+#: siteorigin-panels.php:262
+msgid "Install the missing widget"
+msgstr ""
+
+#: siteorigin-panels.php:853
+msgid "Edit Home Page"
+msgstr ""
+
+#: siteorigin-panels.php:965
+msgid "Untitled"
+msgstr ""
+
+#: siteorigin-panels.php:966
+msgid "Unpublished"
+msgstr ""
+
+#: siteorigin-panels.php:971
+#, php-format
+msgid "Clone Page: %s"
+msgstr ""
+
+#: siteorigin-panels.php:984
+msgid "Clone: Current Home Page"
+msgstr ""
+
+#: siteorigin-panels.php:1003
+msgid "Recommended Plugins and Widgets"
+msgstr ""
+
+#: siteorigin-panels.php:1004
+msgid "Free plugins that work well with Page Builder"
+msgstr ""
+
+#: siteorigin-panels.php:1097
+msgid "This widget is not available, please install the missing plugin."
+msgstr ""
+
+#: siteorigin-panels.php:1169
+msgid "Support Forum"
+msgstr ""
+
+#: tpl/admin-home-page.php:7
+msgid "Custom Home Page"
+msgstr ""
+
+#: tpl/admin-home-page.php:10
+msgid "ON"
+msgstr ""
+
+#: tpl/admin-home-page.php:11
+msgid "OFF"
+msgstr ""
+
+#: tpl/admin-home-page.php:24
+#, php-format
+msgid "Home page updated. View page"
+msgstr ""
+
+#: tpl/admin-home-page.php:31
+msgid "Preview Changes"
+msgstr ""
+
+#: tpl/admin-home-page.php:36
+msgid "Save Home Page"
+msgstr ""
+
+#: tpl/admin-home-page.php:44
+msgid "This interface requires Javascript"
+msgstr ""
+
+#: tpl/help.php:2
+msgid ""
+"You can use SiteOrigin Page Builder to create home and sub pages, filled "
+"your own widgets."
+msgstr ""
+
+#: tpl/help.php:3
+msgid "The page layouts are responsive and fully customizable."
+msgstr ""
+
+#: tpl/help.php:8
+#, php-format
+msgid ""
+"Read the full documentation on SiteOrigin."
+msgstr ""
+
+#: tpl/help.php:9
+#, php-format
+msgid ""
+"Ask a question on our support forum if you "
+"need help and sign up to our newsletter to "
+"stay up to date with future developments."
+msgstr ""
+
+#: tpl/metabox-panels.php:14
+msgid "Add Widget"
+msgstr ""
+
+#: tpl/metabox-panels.php:15 tpl/metabox-panels.php:59
+msgid "Add Row"
+msgstr ""
+
+#: tpl/metabox-panels.php:17
+msgid "Prebuilt Layouts"
+msgstr ""
+
+#: tpl/metabox-panels.php:20
+msgid "Switch to Editor"
+msgstr ""
+
+#: tpl/metabox-panels.php:27
+msgid "Add New Widget"
+msgstr ""
+
+#: tpl/metabox-panels.php:60 widgets/basic.php:76
+msgid "Columns"
+msgstr ""
+
+#: tpl/metabox-panels.php:67
+msgid "Insert Prebuilt Page Layout"
+msgstr ""
+
+#: tpl/metabox-panels.php:68
+msgid "Page Layout"
+msgstr ""
+
+#: tpl/metabox-panels.php:70
+msgid "Select Layout"
+msgstr ""
+
+#: tpl/metabox-panels.php:74
+msgid "Untitled Layout"
+msgstr ""
+
+#: tpl/metabox-panels.php:83
+msgid "Row Visual Style"
+msgstr ""
+
+#: tpl/options.php:11
+msgid "General"
+msgstr ""
+
+#: tpl/options.php:15
+msgid "Post Types"
+msgstr ""
+
+#: tpl/options.php:25
+msgid "Copy Content"
+msgstr ""
+
+#: tpl/options.php:26
+msgid "Copy content from Page Builder into the standard content editor."
+msgstr ""
+
+#: tpl/options.php:32
+msgid "Animations"
+msgstr ""
+
+#: tpl/options.php:33
+msgid "Disable animations for improved performance."
+msgstr ""
+
+#: tpl/options.php:39
+msgid "Bundled Widgets"
+msgstr ""
+
+#: tpl/options.php:40
+msgid "Include the bundled widgets."
+msgstr ""
+
+#: tpl/options.php:48
+msgid "Display"
+msgstr ""
+
+#: tpl/options.php:57
+msgid "Responsive Layout"
+msgstr ""
+
+#: tpl/options.php:58
+msgid "Should the layout collapse for mobile devices."
+msgstr ""
+
+#: tpl/options.php:64
+msgid "Mobile Width"
+msgstr ""
+
+#: tpl/options.php:70
+msgid "Row Bottom Margin"
+msgstr ""
+
+#: tpl/options.php:76
+msgid "Cell Side Margins"
+msgstr ""
+
+#: tpl/options.php:87
+msgid "Save Settings"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:3
+#: video/jplayer/skins/siteorigin/gui.php:3
+msgid "play"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:4
+#: video/jplayer/skins/siteorigin/gui.php:4
+msgid "pause"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:5
+#: video/jplayer/skins/siteorigin/gui.php:5
+msgid "stop"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:7
+#: video/jplayer/skins/siteorigin/gui.php:7
+msgid "full screen"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:8
+#: video/jplayer/skins/siteorigin/gui.php:8
+msgid "restore screen"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:10
+msgid "mute"
+msgstr ""
+
+#: video/jplayer/skins/premium/gui.php:11
+msgid "unmute"
+msgstr ""
+
+#: widgets/basic.php:7
+msgid "Gallery (PB)"
+msgstr ""
+
+#: widgets/basic.php:9
+msgid "Displays a gallery."
+msgstr ""
+
+#: widgets/basic.php:48
+msgid "Gallery Images"
+msgstr ""
+
+#: widgets/basic.php:49
+msgid "edit gallery"
+msgstr ""
+
+#: widgets/basic.php:53
+msgid ""
+"Comma separated attachment IDs. Defaults to all current page's attachments."
+msgstr ""
+
+#: widgets/basic.php:57
+msgid "Image Size"
+msgstr ""
+
+#: widgets/basic.php:60
+msgid "Large"
+msgstr ""
+
+#: widgets/basic.php:61
+msgid "Medium"
+msgstr ""
+
+#: widgets/basic.php:62
+msgid "Thumbnail"
+msgstr ""
+
+#: widgets/basic.php:63
+msgid "Full"
+msgstr ""
+
+#: widgets/basic.php:71
+msgid "Gallery Type"
+msgstr ""
+
+#: widgets/basic.php:81
+msgid "Link To"
+msgstr ""
+
+#: widgets/basic.php:83
+msgid "Attachment Page"
+msgstr ""
+
+#: widgets/basic.php:84
+msgid "File"
+msgstr ""
+
+#: widgets/basic.php:85 widgets/basic.php:145 widgets/basic.php:455
+#: widgets/widgets.php:598
+msgid "None"
+msgstr ""
+
+#: widgets/basic.php:97
+msgid "Post Content (PB)"
+msgstr ""
+
+#: widgets/basic.php:99
+msgid "Displays some form of post content form the current post."
+msgstr ""
+
+#: widgets/basic.php:146 widgets/basic.php:420
+#: widgets/widgets/call-to-action/call-to-action.php:15
+#: widgets/widgets/list/list.php:15 widgets/widgets/price-box/price-box.php:15
+msgid "Title"
+msgstr ""
+
+#: widgets/basic.php:147
+msgid "Featured Image"
+msgstr ""
+
+#: widgets/basic.php:152
+msgid "Display Content"
+msgstr ""
+
+#: widgets/basic.php:167
+msgid "Image (PB)"
+msgstr ""
+
+#: widgets/basic.php:169
+msgid "Displays a simple image."
+msgstr ""
+
+#: widgets/basic.php:202 widgets/widgets/animated-image/animated-image.php:15
+msgid "Image URL"
+msgstr ""
+
+#: widgets/basic.php:206 widgets/widgets/button/button.php:19
+msgid "Destination URL"
+msgstr ""
+
+#: widgets/basic.php:222
+msgid "Post Loop (PB)"
+msgstr ""
+
+#: widgets/basic.php:224
+msgid "Displays a post loop."
+msgstr ""
+
+#: widgets/basic.php:409
+msgid "Your theme doesn't have any post loops."
+msgstr ""
+
+#: widgets/basic.php:424
+msgid "Template"
+msgstr ""
+
+#: widgets/basic.php:439 widgets/widgets.php:584
+msgid "Post Type"
+msgstr ""
+
+#: widgets/basic.php:448 widgets/widgets.php:591
+msgid "Posts Per Page"
+msgstr ""
+
+#: widgets/basic.php:453 widgets/widgets.php:596
+msgid "Order By"
+msgstr ""
+
+#: widgets/basic.php:456 widgets/widgets.php:599
+msgid "Post ID"
+msgstr ""
+
+#: widgets/basic.php:457 widgets/widgets.php:600
+msgid "Author"
+msgstr ""
+
+#: widgets/basic.php:458 widgets/basic.php:459 widgets/widgets.php:601
+#: widgets/widgets.php:602 widgets/widgets/testimonial/testimonial.php:15
+msgid "Name"
+msgstr ""
+
+#: widgets/basic.php:460 widgets/widgets.php:603
+msgid "Date"
+msgstr ""
+
+#: widgets/basic.php:461 widgets/widgets.php:604
+msgid "Modified"
+msgstr ""
+
+#: widgets/basic.php:462 widgets/widgets.php:605
+msgid "Parent"
+msgstr ""
+
+#: widgets/basic.php:463 widgets/widgets.php:606
+msgid "Random"
+msgstr ""
+
+#: widgets/basic.php:464 widgets/widgets.php:607
+msgid "Comment Count"
+msgstr ""
+
+#: widgets/basic.php:465 widgets/basic.php:466 widgets/widgets.php:608
+msgid "Menu Order"
+msgstr ""
+
+#: widgets/basic.php:467
+msgid "Post In Order"
+msgstr ""
+
+#: widgets/basic.php:472 widgets/widgets.php:614
+msgid "Order"
+msgstr ""
+
+#: widgets/basic.php:474 widgets/widgets.php:617
+msgid "Descending"
+msgstr ""
+
+#: widgets/basic.php:475 widgets/widgets.php:616
+msgid "Ascending"
+msgstr ""
+
+#: widgets/basic.php:480 widgets/widgets.php:623
+msgid "Sticky Posts"
+msgstr ""
+
+#: widgets/basic.php:483 widgets/widgets.php:626
+msgid "Ignore Sticky"
+msgstr ""
+
+#: widgets/basic.php:484 widgets/widgets.php:627
+msgid "Exclude Sticky"
+msgstr ""
+
+#: widgets/basic.php:485 widgets/widgets.php:628
+msgid "Only Sticky"
+msgstr ""
+
+#: widgets/basic.php:490
+msgid "More Link "
+msgstr ""
+
+#: widgets/basic.php:492
+msgid "If the template supports it, cut posts and display the more link."
+msgstr ""
+
+#: widgets/basic.php:496
+msgid "Additional "
+msgstr ""
+
+#: widgets/basic.php:498 widgets/widgets.php:635
+#, php-format
+msgid ""
+"Additional query arguments. See query_posts."
+msgstr ""
+
+#: widgets/basic.php:512
+msgid "Embedded Video (PB)"
+msgstr ""
+
+#: widgets/basic.php:514
+msgid "Embeds a video."
+msgstr ""
+
+#: widgets/basic.php:552
+msgid "Video"
+msgstr ""
+
+#: widgets/basic.php:568
+msgid "Self Hosted Video (PB)"
+msgstr ""
+
+#: widgets/basic.php:570
+msgid "A self hosted video player."
+msgstr ""
+
+#: widgets/basic.php:645
+msgid "Video URL"
+msgstr ""
+
+#: widgets/basic.php:649
+msgid "Poster URL"
+msgstr ""
+
+#: widgets/basic.php:651
+msgid "An image that displays before the video starts playing."
+msgstr ""
+
+#: widgets/basic.php:654
+msgid "Skin"
+msgstr ""
+
+#: widgets/basic.php:656
+msgid "SiteOrigin"
+msgstr ""
+
+#: widgets/basic.php:657
+msgid "Premium Pixels"
+msgstr ""
+
+#: widgets/basic.php:661
+msgid "Aspect Ratio"
+msgstr ""
+
+#: widgets/basic.php:663
+msgid "1.777 is HD standard."
+msgstr ""
+
+#: widgets/basic.php:668
+msgid "Auto Play Video"
+msgstr ""
+
+#: widgets/widgets.php:204
+msgid "Style"
+msgstr ""
+
+#: widgets/widgets.php:228
+#, php-format
+msgid "%s Style"
+msgstr ""
+
+#: widgets/widgets.php:634
+msgid "Additional Arguments"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:6
+msgid "Animated Image (PB)"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:8
+msgid "An image that animates in when it enters the screen."
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:19
+msgid "Animation"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:21
+msgid "Fade In"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:22
+msgid "Slide Up"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:23
+msgid "Slide Down"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:24
+msgid "Slide Left"
+msgstr ""
+
+#: widgets/widgets/animated-image/animated-image.php:25
+msgid "Slide Right"
+msgstr ""
+
+#: widgets/widgets/button/button.php:6
+msgid "Button (PB)"
+msgstr ""
+
+#: widgets/widgets/button/button.php:8
+msgid "A simple button"
+msgstr ""
+
+#: widgets/widgets/button/button.php:15 widgets/widgets/list/list.php:19
+#: widgets/widgets/testimonial/testimonial.php:27
+msgid "Text"
+msgstr ""
+
+#: widgets/widgets/button/button.php:23
+#: widgets/widgets/call-to-action/call-to-action.php:31
+#: widgets/widgets/price-box/price-box.php:44
+#: widgets/widgets/testimonial/testimonial.php:35
+msgid "Open In New Window"
+msgstr ""
+
+#: widgets/widgets/button/button.php:27
+msgid "Button Alignment"
+msgstr ""
+
+#: widgets/widgets/button/button.php:29
+msgid "Left"
+msgstr ""
+
+#: widgets/widgets/button/button.php:30
+msgid "Right"
+msgstr ""
+
+#: widgets/widgets/button/button.php:31
+msgid "Center"
+msgstr ""
+
+#: widgets/widgets/button/button.php:32
+msgid "Justify"
+msgstr ""
+
+#: widgets/widgets/call-to-action/call-to-action.php:6
+msgid "Call To Action (PB)"
+msgstr ""
+
+#: widgets/widgets/call-to-action/call-to-action.php:8
+msgid "A Call to Action block"
+msgstr ""
+
+#: widgets/widgets/call-to-action/call-to-action.php:19
+msgid "Sub Title"
+msgstr ""
+
+#: widgets/widgets/call-to-action/call-to-action.php:23
+#: widgets/widgets/price-box/price-box.php:36
+msgid "Button Text"
+msgstr ""
+
+#: widgets/widgets/call-to-action/call-to-action.php:27
+#: widgets/widgets/price-box/price-box.php:40
+msgid "Button URL"
+msgstr ""
+
+#: widgets/widgets/call-to-action/call-to-action.php:37
+#: widgets/widgets/price-box/price-box.php:49
+msgid "Button"
+msgstr ""
+
+#: widgets/widgets/list/list.php:6
+msgid "List (PB)"
+msgstr ""
+
+#: widgets/widgets/list/list.php:8 widgets/widgets/price-box/price-box.php:8
+#: widgets/widgets/testimonial/testimonial.php:8
+msgid "Displays a bullet list of elements"
+msgstr ""
+
+#: widgets/widgets/list/list.php:20 widgets/widgets/price-box/price-box.php:32
+msgid "Start each new point with an asterisk (*)"
+msgstr ""
+
+#: widgets/widgets/price-box/price-box.php:6
+msgid "Price Box (PB)"
+msgstr ""
+
+#: widgets/widgets/price-box/price-box.php:19
+msgid "Price"
+msgstr ""
+
+#: widgets/widgets/price-box/price-box.php:23
+msgid "Per"
+msgstr ""
+
+#: widgets/widgets/price-box/price-box.php:27
+msgid "Information Text"
+msgstr ""
+
+#: widgets/widgets/price-box/price-box.php:31
+msgid "Features Text"
+msgstr ""
+
+#: widgets/widgets/price-box/price-box.php:50
+msgid "Feature List"
+msgstr ""
+
+#: widgets/widgets/testimonial/testimonial.php:6
+msgid "Testimonial (PB)"
+msgstr ""
+
+#: widgets/widgets/testimonial/testimonial.php:19
+msgid "Location"
+msgstr ""
+
+#: widgets/widgets/testimonial/testimonial.php:23
+msgid "Image"
+msgstr ""
+
+#: widgets/widgets/testimonial/testimonial.php:31
+msgid "URL"
+msgstr ""
diff --git a/license.txt b/license.txt
new file mode 100644
index 000000000..94a9ed024
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 000000000..d685c21af
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,193 @@
+=== Page Builder by SiteOrigin ===
+Contributors: gpriday
+Tags: page builder, responsive, widget, widgets, builder, page, admin, gallery, content, cms, pages, post, css, layout, grid
+Requires at least: 3.7
+Tested up to: 4.0
+Stable tag: trunk
+License: GPLv3
+License URI: http://www.gnu.org/licenses/gpl.html
+Donate link: http://siteorigin.com/page-builder/#donate
+
+Build responsive page layouts using the widgets you know and love using this simple drag and drop page builder.
+
+== Description ==
+
+[vimeo http://vimeo.com/59561067]
+
+WordPress has evolved into a fully functional CMS. Page Builder (previously called Panels) completes the transition by giving you a way to create responsive column layouts using the widgets you know and love.
+
+= Use Your Widgets =
+
+You know widgets. They're the things you add to your sidebars. Page Builder makes all your widgets even more useful by turning them into the building blocks of your pages.
+
+We've included a few useful widgets, but it works with a lot of other widgets and plugins out there.
+
+= Works with Most Themes =
+
+Page Builder works with most well made themes. The only requirement is that your theme supports pages. And if your theme is responsive, change a few settings and boom, your layouts will work with your theme and collapse into a single column on mobile devices.
+
+There are loads free and premium themes that work with the Page Builder, we have our own collection of [free themes](http://siteorigin.com/) if you'd like to use one of ours.
+
+Page Builder [Documentation](http://siteorigin.com/page-builder/documentation/) is available on SiteOrigin and we offer free support on our [support forum](http://siteorigin.com/threads/plugin-page-builder/). If you're having strange issues, try following [this guide](http://siteorigin.com/troubleshooting/identifying-plugin-conflicts/).
+
+= Bundled Widgets =
+
+To get you started, we've include a few widgets:
+
+* Gallery widget for inserting image galleries.
+* Image widget for inserting standard images.
+* Self hosted video widget for embedding your own videos.
+* Post Loop to display a list of posts. This requires that your theme supports it.
+
+As well as some essential page elements widgets:
+
+* Button
+* Call to Action
+* List
+* Price Box
+* Animated Image
+* Testimonial
+
+= 3rd Party Widgets =
+
+Most standard widgets work with Page Builder, but here are some of our favorites.
+
+* [SiteOrigin Widget Bundle](http://wordpress.org/plugins/so-widgets-bundle/) for growing collection of widgets like buttons, price tables and images.
+* [Black Studio TinyMCE](http://wordpress.org/plugins/black-studio-tinymce-widget/) for a visual content editing widget.
+* [Meta Slider](http://wordpress.org/plugins/ml-slider/) for a responsive slider widget.
+
+== Installation ==
+
+1. Upload and install Page Builder in the same way you'd install any other plugin.
+2. Read the [usage documentation](http://siteorigin.com/page-builder/documentation/) on SiteOrigin.
+
+== Screenshots ==
+
+1. The page builder interface.
+2. Adding a new widget. This includes a live search filter to help you keep control if you have a lot of widgets.
+3. Editing a widget's settings.
+4. Easily undo mistakes.
+
+== Documentation ==
+
+[Documentation](http://siteorigin.com/page-builder/documentation/) is available on SiteOrigin.
+
+== Frequently Asked Questions ==
+
+= How do I move a site created with Page Builder from one server to another? =
+
+We recommend the [duplicator plugin](https://wordpress.org/plugins/duplicator/). We've tested it in several instances and it always works well with Page Builder data.
+
+= Can I bundle Page Builder with my theme? =
+
+Yes, provided your theme is licensed under GPL or a compatible license. If you're publishing your theme on ThemeForest, you must select the GPL license instead of their regular license.
+
+Page Builder is actively developed and updated, so generally I'd recommend that you have your users install the actual plugin so they can receive updates. You can try [TGM Plugin Activation](http://tgmpluginactivation.com/).
+
+= Will plugin X work with Page Builder? =
+
+I've tried to ensure that Page Builder is compatible with most plugin widgets. It's best to just download Page Builder and test for yourself.
+
+== Changelog ==
+
+= 2.0 =
+* Complete rewrite of Page Builder Javascript using Backbone.
+
+= 1.5.4 =
+* Readded inline CSS setting.
+* Improved handling of missing widgets in prebuilt layouts.
+
+= 1.5.3 =
+* Fixed post loop widget issue.
+* Fixed settings issue.
+
+= 1.5.2 =
+* Changed to custom settings system to fix a few settings bugs.
+* Added option to display more link in post loop widget.
+* Fixed SSL in widget images.
+
+= 1.5.1 =
+* Compatibility with WordPress 4.0 - needed to change how tabs function.
+* Compatibility with Black Studio TinyMCE Widget 2.0.
+* Namespaced Tooltip to avoid conflicts.
+
+= 1.5 =
+* Increased size of widget dialog boxes.
+* Updated incompatible plugins list.
+* Updated to latest version of Chosen.
+* Custom Home Page feature now uses standard pages.
+* Improvements to preview handling.
+
+= 1.4.12 =
+* Improved how missing widgets are handled.
+* General code clean up.
+* Prebuilt layouts are no longer all filtered by siteorigin_panels_data. Filtered by siteorigin_panels_prebuilt_layout when fetched.
+* Added more hooks and filters.
+* Incompatible plugins now includes more link to give details about incompatibility.
+
+= 1.4.11 =
+* Fixed: Issue with setting up a home page, switching themes, then not being able to disable the home page.
+* Updated to be compatible with latest Black Studio TinyMCE widget.
+* Added a plugin incompatibility check with an admin notice.
+* Improved bundled language files.
+
+= 1.4.10 =
+* Fixed: Fixed z-indexes so that TinyMCE dropdowns (like formatting) aren't hidden.
+
+= 1.4.9 =
+* Fixed: jQuery UI dialog wasn't being enqueued properly in WordPress 3.9.
+
+= 1.4.8 =
+* Updated Post Loop widget so it now accepts post__in in additional args field.
+* Added update notification.
+* Added filters for before and after the row content.
+* Removed references to legacy widgets.
+
+= 1.4.7 =
+* Fixed size problem in gallery widget.
+* Compatibility fixes with WordPress 3.9.
+
+= 1.4.6 =
+* Widgets are now only run through their update function when modified.
+* Fixed gallery widget.
+
+= 1.4.5 =
+* Fixed an issue with copy content.
+* Improved handling of styles in prebuilt layouts.
+* Improved error handling in Javascript.
+* Fixed issue with checkboxes.
+
+= 1.4.4 =
+* Generating Page Builder content in admin is now generated with a separate request to properly handle fatal errors from widgets.
+* Fixed potential issue when loading home page interface.
+* Added a way for themes to specify more advanced row styles.
+* Dialogs and widget forms are now only loaded when needed in order to improve performance on large pages.
+* Fixed several performance bottle necks.
+* Page Builder data is now saved with auto save and revisions.
+
+= 1.4.3 =
+* Improved HTML5 validation be moving styles to header and footer.
+* Basic improvements to memory efficiency.
+* Black Studio TinyMCE height set to 350 pixels by default.
+* Fixed: Black Studio TinyMCE update error.
+
+= 1.4.2 =
+* All existing widget forms are loaded with the initial interface, rather than through AJAX. Improves performance.
+* Added safety check to ensure Page Builder data loaded before into the interface before saving into the database. Helps prevent content loss.
+* Small usability improvements.
+* Fixed: Embedded video widget.
+* Fixed: Conflict with GPP Slideshow plugin.
+* Fixed: Possible z-index conflicts with other plugins that have jQuery UI CSS.
+* Fixed: Constant notification about autosave being more recent than current version.
+
+= 1.4.1 =
+* Fixed: Issue that was removing content for widgets with a lot of data.
+* Fixed: Issue with duplicating widgets.
+
+= 1.4.0 =
+* Changed how widget forms are loaded to improve page load times.
+* Several improvements to increase compatibility with various plugins and widgets.
+* Properly handle widgets with form arrays.
+* CSS fixes.
+* Fixed compatibility issues with Black Studio TinyMCE.
+* Added more development hooks and filters.
\ No newline at end of file
diff --git a/siteorigin-panels.php b/siteorigin-panels.php
new file mode 100644
index 000000000..f7d416a2d
--- /dev/null
+++ b/siteorigin-panels.php
@@ -0,0 +1,1259 @@
+ __( 'Home', 'siteorigin-panels' ),
+ 'post_status' => $_POST['siteorigin_panels_home_enabled'] == 'true' ? 'publish' : 'draft',
+ 'post_type' => 'page',
+ 'comment_status' => 'closed',
+ ) );
+ update_option( 'siteorigin_panels_home_page_id', $page_id );
+ }
+ else {
+ $page_id = get_option( 'siteorigin_panels_home_page_id' );
+ }
+
+ // Save the updated page data
+ $panels_data = json_decode( wp_unslash( $_POST['panels_data'] ), true);
+ $panels_data['widgets'] = siteorigin_panels_process_raw_widgets($panels_data['widgets']);
+ $panels_data = siteorigin_panels_styles_sanitize_all( $panels_data );
+
+ update_post_meta( $page_id, 'panels_data', $panels_data );
+ update_post_meta( $page_id, '_wp_page_template', siteorigin_panels_setting( 'home-template' ) );
+
+ if( !empty( $_POST['siteorigin_panels_home_enabled'] ) ) {
+ update_option('show_on_front', 'page');
+ update_option('page_on_front', $page_id);
+ wp_publish_post($page_id);
+ }
+ else {
+ // We're disabling this home page
+ if( get_option('page_on_front') == $page_id ) {
+ // Disable the front page display
+ update_option('page_on_front', false);
+
+ if( !get_option( 'page_for_posts' ) ) {
+ update_option( 'show_on_front', 'posts' );
+ }
+ }
+
+ // Change the post status to draft
+ $post = get_post($page_id);
+ if($post->post_status != 'draft') {
+ global $wpdb;
+
+ $wpdb->update( $wpdb->posts, array( 'post_status' => 'draft' ), array( 'ID' => $post->ID ) );
+ clean_post_cache( $post->ID );
+
+ $old_status = $post->post_status;
+ $post->post_status = 'draft';
+ wp_transition_post_status( 'draft', $old_status, $post );
+
+ do_action( 'edit_post', $post->ID, $post );
+ do_action( "save_post_{$post->post_type}", $post->ID, $post, true );
+ do_action( 'save_post', $post->ID, $post, true );
+ do_action( 'wp_insert_post', $post->ID, $post, true );
+ }
+
+ }
+}
+add_action('admin_init', 'siteorigin_panels_save_home_page');
+
+/**
+ * After the theme is switched, change the template on the home page if the theme supports home page functionality.
+ */
+function siteorigin_panels_update_home_on_theme_change(){
+ if( siteorigin_panels_setting( 'home-page' ) && siteorigin_panels_setting( 'home-template' ) && get_option( 'siteorigin_panels_home_page_id' ) ) {
+ // Lets update the home page to use the home template that this theme supports
+ update_post_meta( get_option( 'siteorigin_panels_home_page_id' ), '_wp_page_template', siteorigin_panels_setting( 'home-template' ) );
+ }
+}
+add_action('after_switch_theme', 'siteorigin_panels_update_home_on_theme_change');
+
+/**
+ * @return mixed|void Are we currently viewing the home page
+ */
+function siteorigin_panels_is_home(){
+ $home = ( is_front_page() && is_page() && get_option('show_on_front') == 'page' && get_option('page_on_front') == get_the_ID() && get_post_meta( get_the_ID(), 'panels_data' ) );
+ return apply_filters('siteorigin_panels_is_home', $home);
+}
+
+/**
+ * Check if we're currently viewing a page builder page.
+ *
+ * @param bool $can_edit Also check if the user can edit this page
+ * @return bool
+ */
+function siteorigin_panels_is_panel($can_edit = false){
+ // Check if this is a panel
+ $is_panel = ( siteorigin_panels_is_home() || ( is_singular() && get_post_meta(get_the_ID(), 'panels_data', false) != '' ) );
+ return $is_panel && (!$can_edit || ( (is_singular() && current_user_can('edit_post', get_the_ID())) || ( siteorigin_panels_is_home() && current_user_can('edit_theme_options') ) ));
+}
+
+/**
+ * Render a panel metabox.
+ *
+ * @param $post
+ */
+function siteorigin_panels_metabox_render( $post ) {
+ $panels_data = siteorigin_panels_get_current_admin_panels_data();
+ include plugin_dir_path(__FILE__) . 'tpl/metabox-panels.php';
+}
+
+/**
+ * Enqueue the panels admin scripts
+ *
+ * @action admin_print_scripts-post-new.php
+ * @action admin_print_scripts-post.php
+ * @action admin_print_scripts-appearance_page_so_panels_home_page
+ */
+function siteorigin_panels_admin_enqueue_scripts($prefix) {
+ $screen = get_current_screen();
+
+ if ( ( $screen->base == 'post' && in_array( $screen->id, siteorigin_panels_setting('post-types') ) ) || $screen->base == 'appearance_page_so_panels_home_page' || $screen->base == 'widgets') {
+
+ wp_enqueue_script( 'so-panels-admin', plugin_dir_url(__FILE__) . 'js/siteorigin-panels.js', array( 'jquery', 'jquery-ui-resizable', 'jquery-ui-sortable', 'jquery-ui-draggable', 'underscore', 'backbone' ), SITEORIGIN_PANELS_VERSION, true );
+ wp_enqueue_script( 'so-panels-admin-styles', plugin_dir_url(__FILE__) . 'js/siteorigin-panels-styles.js', array( 'so-panels-admin', 'jquery', 'underscore', 'backbone', 'wp-color-picker' ), SITEORIGIN_PANELS_VERSION, true );
+
+ if( $screen->base != 'widgets' ) {
+ // We don't use the history browser and live editor in the widgets interface
+ wp_enqueue_script( 'so-panels-admin-history', plugin_dir_url(__FILE__) . 'js/siteorigin-panels-history.js', array( 'so-panels-admin', 'jquery', 'underscore', 'backbone' ), SITEORIGIN_PANELS_VERSION, true );
+ wp_enqueue_script( 'so-panels-admin-live-editor', plugin_dir_url(__FILE__) . 'js/siteorigin-panels-live-editor.js', array( 'so-panels-admin', 'jquery', 'underscore', 'backbone' ), SITEORIGIN_PANELS_VERSION, true );
+ }
+
+ add_action('admin_footer', 'siteorigin_panels_js_templates');
+
+ $widgets = siteorigin_panels_get_widgets();
+
+ wp_localize_script( 'so-panels-admin', 'soPanelsOptions', array(
+ 'widgets' => $widgets,
+ 'widget_dialog_tabs' => apply_filters( 'siteorigin_panels_widget_dialog_tabs', array(
+ array(
+ 'title' => __('All Widgets', 'siteorigin-panels'),
+ 'filter' => array( 'installed' => true, 'groups' => '' )
+ )
+ ) ),
+ 'row_layouts' => apply_filters( 'siteorigin_panels_row_layouts', array() ),
+ // General localization messages
+ 'loc' => array(
+ 'missing_widget' => array(
+ 'title' => __('Missing Widget', 'siteorigin-panels'),
+ 'description' => __("Page Builder doesn't know about this widget", 'siteorigin-panels'),
+ ),
+ 'time' => array(
+ 'seconds' => __('%d seconds', 'siteorigin-panels'),
+ 'minutes' => __('%d minutes', 'siteorigin-panels'),
+ 'hours' => __('%d hours', 'siteorigin-panels'),
+
+ 'second' => __('%d second', 'siteorigin-panels'),
+ 'minute' => __('%d minute', 'siteorigin-panels'),
+ 'hour' => __('%d hour', 'siteorigin-panels'),
+
+ 'ago' => __('%s before', 'siteorigin-panels'),
+ 'now' => __('Now', 'siteorigin-panels'),
+ ),
+ 'history' => array(
+ // History messages
+ 'current' => __('Current', 'siteorigin-panels'),
+ 'revert' => __('Original', 'siteorigin-panels'),
+ 'restore' => __('Version restored', 'siteorigin-panels'),
+
+ // Widgets
+ 'widget_deleted' => __('Widget deleted', 'siteorigin-panels'),
+ 'widget_added' => __('Widget added', 'siteorigin-panels'),
+ 'widget_edited' => __('Widget edited', 'siteorigin-panels'),
+ 'widget_duplicated' => __('Widget duplicated', 'siteorigin-panels'),
+ 'widget_moved' => __('Widget moved', 'siteorigin-panels'),
+
+ // Rows
+ 'row_deleted' => __('Row deleted', 'siteorigin-panels'),
+ 'row_added' => __('Row added', 'siteorigin-panels'),
+ 'row_edited' => __('Row edited', 'siteorigin-panels'),
+ 'row_moved' => __('Row moved', 'siteorigin-panels'),
+ 'row_duplicated' => __('Row duplicated', 'siteorigin-panels'),
+
+ // Cells
+ 'cell_resized' => __('Cell resized', 'siteorigin-panels'),
+
+ // Prebuilt
+ 'prebuilt_loaded' => __('Prebuilt layout loaded', 'siteorigin-panels'),
+ ),
+
+ // general localization
+ 'prebuilt_confirm' => __('Are you sure you want to overwrite your current content? This can be undone in the builder history.', 'siteorigin-panels'),
+ 'prebuilt_loading' => __('Loading prebuilt layout', 'siteorigin-panels'),
+ 'confirm_use_builder' => __("Would you like to copy this editor's existing content to Page Builder?", 'siteorigin-panels'),
+ 'layout_widget' => __('Layout Widget', 'siteorigin-panels'),
+ 'dropdown_confirm' => __('Are you sure?', 'siteorigin-panels'),
+ ),
+ ));
+
+ // Let themes and plugins give names and descriptions to missing widgets.
+ global $wp_widget_factory;
+ $missing_widgets = array();
+ if ( !empty( $panels_data['widgets'] ) ) {
+ foreach ( $panels_data['widgets'] as $i => $widget ) {
+
+ // There's a chance the widget was activated by siteorigin_panels_widget_is_missing
+ if ( empty( $wp_widget_factory->widgets[ $widget['info']['class'] ] ) ) {
+ $missing_widgets[$widget['info']['class']] = apply_filters('siteorigin_panels_missing_widget_data', array(
+ 'title' => str_replace( '_', ' ', $widget['info']['class'] ),
+ 'description' => __('Install the missing widget', 'siteorigin-panels'),
+ ), $widget['info']['class']);
+ }
+ }
+ }
+
+ if( !empty($missing_widgets) ) {
+ wp_localize_script( 'so-panels-admin', 'panelsMissingWidgets', $missing_widgets );
+ }
+
+ if( $screen->base != 'widgets' ) {
+ // Render all the widget forms. A lot of widgets use this as a chance to enqueue their scripts
+ $original_post = isset($GLOBALS['post']) ? $GLOBALS['post'] : null; // Make sure widgets don't change the global post.
+ foreach($GLOBALS['wp_widget_factory']->widgets as $class => $widget_obj){
+ ob_start();
+ $widget_obj->form( array() );
+ ob_clean();
+ }
+ $GLOBALS['post'] = $original_post;
+ }
+
+ // This gives panels a chance to enqueue scripts too, without having to check the screen ID.
+ do_action( 'siteorigin_panel_enqueue_admin_scripts' );
+ do_action( 'sidebar_admin_setup' );
+ }
+}
+add_action( 'admin_print_scripts-post-new.php', 'siteorigin_panels_admin_enqueue_scripts' );
+add_action( 'admin_print_scripts-post.php', 'siteorigin_panels_admin_enqueue_scripts' );
+add_action( 'admin_print_scripts-appearance_page_so_panels_home_page', 'siteorigin_panels_admin_enqueue_scripts' );
+add_action( 'admin_print_scripts-widgets.php', 'siteorigin_panels_admin_enqueue_scripts' );
+
+/**
+ * Get an array of all the available widgets.
+ *
+ * @return array
+ */
+function siteorigin_panels_get_widgets(){
+ global $wp_widget_factory;
+ $widgets = array();
+ foreach($wp_widget_factory->widgets as $class => $widget_obj) {
+ $widgets[$class] = array(
+ 'class' => $class,
+ 'title' => !empty($widget_obj->name) ? $widget_obj->name : __('Untitled Widget', 'siteorigin-panels'),
+ 'description' => !empty($widget_obj->widget_options['description']) ? $widget_obj->widget_options['description'] : '',
+ 'installed' => true,
+ 'groups' => array(),
+ );
+
+ // Get Page Builder specific widget options
+ if( isset($widget_obj->widget_options['panels_title']) ) {
+ $widgets[$class]['panels_title'] = $widget_obj->widget_options['panels_title'];
+ }
+ if( isset($widget_obj->widget_options['panels_groups']) ) {
+ $widgets[$class]['groups'] = $widget_obj->widget_options['panels_groups'];
+ }
+ if( isset($widget_obj->widget_options['panels_icon']) ) {
+ $widgets[$class]['icon'] = $widget_obj->widget_options['panels_icon'];
+ }
+
+ }
+
+ // Other plugins can manipulate the list of widgets. Possibly to add recommended widgets
+ $widgets = apply_filters('siteorigin_panels_widgets', $widgets);
+
+ // Sort the widgets alphabetically
+ uasort($widgets, 'siteorigin_panels_widgets_sorter');
+
+ return $widgets;
+}
+
+/**
+ * @param $a
+ * @param $b
+ */
+function siteorigin_panels_widgets_sorter($a, $b){
+ return $a['title'] > $b['title'] ? 1 : -1;
+}
+
+/**
+ * Display the templates for JS in the footer
+ */
+function siteorigin_panels_js_templates(){
+ include plugin_dir_path(__FILE__).'tpl/js-templates.php';
+}
+
+/**
+ * Enqueue the admin panel styles
+ *
+ * @action admin_print_styles-post-new.php
+ * @action admin_print_styles-post.php
+ */
+function siteorigin_panels_admin_enqueue_styles() {
+ $screen = get_current_screen();
+ if ( in_array( $screen->id, siteorigin_panels_setting('post-types') ) || $screen->base == 'appearance_page_so_panels_home_page' || $screen->base == 'widgets') {
+ wp_enqueue_style( 'so-panels-admin', plugin_dir_url(__FILE__) . 'css/admin.css', array( 'wp-color-picker' ), SITEORIGIN_PANELS_VERSION );
+ do_action( 'siteorigin_panel_enqueue_admin_styles' );
+ }
+}
+add_action( 'admin_print_styles-post-new.php', 'siteorigin_panels_admin_enqueue_styles' );
+add_action( 'admin_print_styles-post.php', 'siteorigin_panels_admin_enqueue_styles' );
+add_action( 'admin_print_styles-appearance_page_so_panels_home_page', 'siteorigin_panels_admin_enqueue_styles' );
+add_action( 'admin_print_styles-widgets.php', 'siteorigin_panels_admin_enqueue_styles' );
+
+/**
+ * Add a help tab to pages with panels.
+ */
+function siteorigin_panels_add_help_tab($prefix) {
+ $screen = get_current_screen();
+ if(
+ ( $screen->base == 'post' && ( in_array( $screen->id, siteorigin_panels_setting( 'post-types' ) ) || $screen->id == '') )
+ || ($screen->id == 'appearance_page_so_panels_home_page')
+ ) {
+ $screen->add_help_tab( array(
+ 'id' => 'panels-help-tab', //unique id for the tab
+ 'title' => __( 'Page Builder', 'siteorigin-panels' ), //unique visible title for the tab
+ 'callback' => 'siteorigin_panels_add_help_tab_content'
+ ) );
+ }
+}
+add_action('load-page.php', 'siteorigin_panels_add_help_tab', 12);
+add_action('load-post-new.php', 'siteorigin_panels_add_help_tab', 12);
+add_action('load-appearance_page_so_panels_home_page', 'siteorigin_panels_add_help_tab', 12);
+
+/**
+ * Display the content for the help tab.
+ */
+function siteorigin_panels_add_help_tab_content(){
+ include plugin_dir_path(__FILE__) . 'tpl/help.php';
+}
+
+/**
+ * Save the panels data
+ *
+ * @param $post_id
+ * @param $post
+ *
+ * @action save_post
+ */
+function siteorigin_panels_save_post( $post_id, $post ) {
+ if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
+ if ( empty( $_POST['_sopanels_nonce'] ) || !wp_verify_nonce( $_POST['_sopanels_nonce'], 'save' ) ) return;
+ if ( !current_user_can( 'edit_post', $post_id ) ) return;
+
+ if ( !wp_is_post_revision($post_id) ) {
+ $panels_data = json_decode( wp_unslash( $_POST['panels_data'] ), true);
+ $panels_data['widgets'] = siteorigin_panels_process_raw_widgets($panels_data['widgets']);
+ $panels_data = siteorigin_panels_styles_sanitize_all( $panels_data );
+
+ if( !empty( $panels_data['widgets'] ) ) {
+ update_post_meta( $post_id, 'panels_data', $panels_data );
+ }
+ else {
+ // There are no widgets, so delete the panels data.
+ delete_post_meta( $post_id, 'panels_data' );
+ }
+ }
+ else {
+ // When previewing, we don't need to wp_unslash the panels_data post variable.
+ $panels_data = json_decode( $_POST['panels_data'], true);
+ $panels_data['widgets'] = siteorigin_panels_process_raw_widgets($panels_data['widgets']);
+ $panels_data = siteorigin_panels_styles_sanitize_all( $panels_data );
+
+ // Because of issue #20299, we are going to save the preview into a different variable so we don't overwrite the actual data.
+ // https://core.trac.wordpress.org/panels_data/20299
+ if( !empty( $panels_data['widgets'] ) ) {
+ update_post_meta( $post_id, '_panels_data_preview', $panels_data );
+ }
+ }
+}
+add_action( 'save_post', 'siteorigin_panels_save_post', 10, 2 );
+
+/**
+ * @param $value
+ * @param $post_id
+ * @param $meta_key
+ *
+ * @return mixed
+ */
+function siteorigin_panels_view_post_preview($value, $post_id, $meta_key){
+ if( $meta_key == 'panels_data' && is_preview() && current_user_can( 'edit_post', $post_id ) ) {
+ $panels_preview = get_post_meta($post_id, '_panels_data_preview');
+ return !empty($panels_preview) ? $panels_preview : $value;
+ }
+
+ return $value;
+}
+add_filter('get_post_metadata', 'siteorigin_panels_view_post_preview', 10, 3);
+
+/**
+ * Process raw widgets that have come from the Page Builder front end.
+ *
+ * @param $widgets
+ */
+function siteorigin_panels_process_raw_widgets($widgets) {
+ for($i = 0; $i < count($widgets); $i++) {
+
+ $info = isset($widgets[$i]['panels_info']) ? $widgets[$i]['panels_info'] : $widgets[$i]['info'];
+ unset($widgets[$i]['info']);
+
+ if( !empty($info['raw']) ) {
+ if ( class_exists( $info['class'] ) && method_exists( $info['class'], 'update' ) ) {
+ $the_widget = new $info['class'];
+ $widgets[$i] = $the_widget->update( $widgets[$i], $widgets[$i] );
+ unset($info['raw']);
+ }
+ }
+
+ $widgets[$i]['panels_info'] = $info;
+
+ }
+
+ return $widgets;
+}
+
+/**
+ * Get the home page panels layout data.
+ *
+ * @return mixed|void
+ */
+function siteorigin_panels_get_home_page_data(){
+ $panels_data = get_option('siteorigin_panels_home_page', null);
+ if( is_null( $panels_data ) ){
+ // Load the default layout
+ $layouts = apply_filters( 'siteorigin_panels_prebuilt_layouts', array() );
+ $panels_data = !empty($layouts['default_home']) ? $layouts['default_home'] : current($layouts);
+ }
+
+ return $panels_data;
+}
+
+/**
+ * Get the Page Builder data for the current admin page.
+ *
+ * @return array
+ */
+function siteorigin_panels_get_current_admin_panels_data(){
+ $screen = get_current_screen();
+
+ // Localize the panels with the panels data
+ if($screen->base == 'appearance_page_so_panels_home_page'){
+ $page_id = get_option( 'siteorigin_panels_home_page_id' );
+ if( !empty($page_id) ) $panels_data = get_post_meta( $page_id, 'panels_data', true );
+ else $panels_data = null;
+
+ if( is_null( $panels_data ) ){
+ // Load the default layout
+ $layouts = apply_filters( 'siteorigin_panels_prebuilt_layouts', array() );
+
+ $home_name = siteorigin_panels_setting('home-page-default') ? siteorigin_panels_setting('home-page-default') : 'home';
+ $panels_data = !empty($layouts[$home_name]) ? $layouts[$home_name] : current($layouts);
+ }
+
+ $panels_data = apply_filters( 'siteorigin_panels_data', $panels_data, 'home');
+ }
+ else{
+ global $post;
+ $panels_data = get_post_meta( $post->ID, 'panels_data', true );
+ $panels_data = apply_filters( 'siteorigin_panels_data', $panels_data, $post->ID );
+ }
+
+ if ( empty( $panels_data ) ) $panels_data = array();
+
+ return $panels_data;
+}
+
+/**
+ * Generate the CSS for the page layout.
+ *
+ * @param $post_id
+ * @param $panels_data
+ * @return string
+ */
+function siteorigin_panels_generate_css($post_id, $panels_data){
+ // Exit if we don't have panels data
+ if ( empty( $panels_data ) || empty( $panels_data['grids'] ) ) return;
+
+ // Get some of the default settings
+ $settings = siteorigin_panels_setting();
+ $panels_mobile_width = $settings['mobile-width'];
+ $panels_margin_bottom = $settings['margin-bottom'];
+
+ $css = new SiteOrigin_Panels_Css_Builder();
+
+ $ci = 0;
+ foreach ( $panels_data['grids'] as $gi => $grid ) {
+
+ $cell_count = intval( $grid['cells'] );
+
+ // Add the cell sizing
+ for ( $i = 0; $i < $cell_count; $i++ ) {
+ $cell = $panels_data['grid_cells'][$ci++];
+
+ if ( $cell_count > 1 ) {
+ $width = round( $cell['weight'] * 100, 3 ) . '%';
+ $width = apply_filters('siteorigin_panels_css_cell_width', $width, $grid, $gi, $cell, $ci - 1, $panels_data, $post_id);
+
+ // Add the width and ensure we have correct formatting for CSS.
+ $css->add_cell_css($post_id, $gi, $i, '', array(
+ 'width' => str_replace(',', '.', $width)
+ ));
+ }
+ }
+
+ // Add the bottom margin to any grids that aren't the last
+ if($gi != count($panels_data['grids'])-1){
+ // Filter the bottom margin for this row with the arguments
+ $css->add_row_css($post_id, $gi, '', array(
+ 'margin-bottom' => apply_filters('siteorigin_panels_css_row_margin_bottom', $panels_margin_bottom.'px', $grid, $gi, $panels_data, $post_id)
+ ));
+ }
+
+ if ( $cell_count > 1 ) {
+ $css->add_cell_css($post_id, $gi, false, '', array(
+ // Float right for RTL
+ 'float' => !is_rtl() ? 'left' : 'right'
+ ));
+ }
+
+ if ( $settings['responsive'] ) {
+ // Mobile Responsive
+ $css->add_cell_css($post_id, $gi, false, '', array(
+ 'float' => 'none',
+ 'width' => 'auto'
+ ), $panels_mobile_width);
+
+ for ( $i = 0; $i < $cell_count; $i++ ) {
+ if ( $i != $cell_count - 1 ) {
+ $css->add_cell_css($post_id, $gi, $i, '', array(
+ 'margin-bottom' => $panels_margin_bottom . 'px',
+ ), $panels_mobile_width);
+ }
+ }
+ }
+ }
+
+ if( $settings['responsive'] ) {
+ // Add CSS to prevent overflow on mobile resolution.
+ $css->add_row_css($post_id, false, '', array(
+ 'margin-left' => 0,
+ 'margin-right' => 0,
+ ), $panels_mobile_width);
+
+ $css->add_cell_css($post_id, false, false, '', array(
+ 'padding' => 0,
+ ), $panels_mobile_width);
+ }
+
+ // Add the bottom margins
+ $css->add_cell_css($post_id, false, false, '.panel', array(
+ 'margin-bottom' => $panels_margin_bottom.'px'
+ ));
+ $css->add_cell_css($post_id, false, false, '.panel:last-child', array(
+ 'margin-bottom' => 0
+ ));
+
+ // Let other plugins customize various aspects of the rows (grids)
+ foreach ( $panels_data['grids'] as $gi => $grid ) {
+ // Rows with only one cell don't need gutters
+ if($grid['cells'] <= 1) continue;
+
+ // Let other themes and plugins change the gutter.
+ $gutter = apply_filters('siteorigin_panels_css_row_gutter', $settings['margin-sides'].'px', $grid, $gi, $panels_data);
+
+ if( !empty($gutter) ) {
+ // We actually need to find half the gutter.
+ preg_match('/([0-9\.,]+)(.*)/', $gutter, $match);
+ if( !empty( $match[1] ) ) {
+ $margin_half = (floatval($match[1])/2) . $match[2];
+ $css->add_row_css($post_id, $gi, '', array(
+ 'margin-left' => '-' . $margin_half,
+ 'margin-right' => '-' . $margin_half,
+ ) );
+ $css->add_cell_css($post_id, $gi, false, '', array(
+ 'padding-left' => $margin_half,
+ 'padding-right' => $margin_half,
+ ) );
+
+ }
+ }
+ }
+
+ // Let other plugins and components filter the CSS object.
+ $css = apply_filters('siteorigin_panels_css_object', $css, $panels_data, $post_id);
+ return $css->get_css();
+}
+
+/**
+ * Prepare the content of the page early on so widgets can enqueue their scripts and styles
+ */
+function siteorigin_panels_prepare_single_post_content(){
+ if( is_singular() ) {
+ global $siteorigin_panels_cache;
+ if( empty($siteorigin_panels_cache[ get_the_ID() ] ) ) {
+ $siteorigin_panels_cache[ get_the_ID() ] = siteorigin_panels_render( get_the_ID() );
+ }
+ }
+}
+add_action('wp_enqueue_scripts', 'siteorigin_panels_prepare_single_post_content');
+
+/**
+ * Filter the content of the panel, adding all the widgets.
+ *
+ * @param $content
+ * @return string
+ *
+ * @filter the_content
+ */
+function siteorigin_panels_filter_content( $content ) {
+ global $post;
+
+ if ( empty( $post ) ) return $content;
+ if ( !apply_filters( 'siteorigin_panels_filter_content_enabled', true ) ) return $content;
+ if ( in_array( $post->post_type, siteorigin_panels_setting('post-types') ) ) {
+ $panel_content = siteorigin_panels_render( $post->ID );
+
+ if ( !empty( $panel_content ) ) $content = $panel_content;
+ }
+
+ return $content;
+}
+add_filter( 'the_content', 'siteorigin_panels_filter_content' );
+
+
+/**
+ * Render the panels
+ *
+ * @param int|string|bool $post_id The Post ID or 'home'.
+ * @param bool $enqueue_css Should we also enqueue the layout CSS.
+ * @param array|bool $panels_data Existing panels data. By default load from settings or post meta.
+ * @return string
+ */
+function siteorigin_panels_render( $post_id = false, $enqueue_css = true, $panels_data = false ) {
+ if( empty($post_id) ) $post_id = get_the_ID();
+
+ global $siteorigin_panels_current_post;
+ $old_current_post = $siteorigin_panels_current_post;
+ $siteorigin_panels_current_post = $post_id;
+
+ // Try get the cached panel from in memory cache.
+ global $siteorigin_panels_cache;
+ if(!empty($siteorigin_panels_cache) && !empty($siteorigin_panels_cache[$post_id]))
+ return $siteorigin_panels_cache[$post_id];
+
+ if( empty($panels_data) ) {
+ if( strpos($post_id, 'prebuilt:') === 0) {
+ list($null, $prebuilt_id) = explode(':', $post_id, 2);
+ $layouts = apply_filters('siteorigin_panels_prebuilt_layouts', array());
+ $panels_data = !empty($layouts[$prebuilt_id]) ? $layouts[$prebuilt_id] : array();
+ }
+ else if($post_id == 'home'){
+ $panels_data = get_post_meta( get_option('siteorigin_panels_home_page_id'), 'panels_data', true );
+
+ if( is_null($panels_data) ){
+ // Load the default layout
+ $layouts = apply_filters('siteorigin_panels_prebuilt_layouts', array());
+ $prebuilt_id = siteorigin_panels_setting('home-page-default') ? siteorigin_panels_setting('home-page-default') : 'home';
+
+ $panels_data = !empty($layouts[$prebuilt_id]) ? $layouts[$prebuilt_id] : current($layouts);
+ }
+ }
+ else{
+ if ( post_password_required($post_id) ) return false;
+ $panels_data = get_post_meta( $post_id, 'panels_data', true );
+ }
+ }
+
+ $panels_data = apply_filters( 'siteorigin_panels_data', $panels_data, $post_id );
+ if( empty( $panels_data ) || empty( $panels_data['grids'] ) ) return '';
+
+ if( is_rtl() ) $panels_data = siteorigin_panels_make_rtl( $panels_data );
+
+ // Create the skeleton of the grids
+ $grids = array();
+ if( !empty( $panels_data['grids'] ) && !empty( $panels_data['grids'] ) ) {
+ foreach ( $panels_data['grids'] as $gi => $grid ) {
+ $gi = intval( $gi );
+ $grids[$gi] = array();
+ for ( $i = 0; $i < $grid['cells']; $i++ ) {
+ $grids[$gi][$i] = array();
+ }
+ }
+ }
+
+ // We need this to migrate from the old $panels_data that put widget meta into the "info" key instead of "panels_info"
+ if( !empty( $panels_data['widgets'] ) && is_array($panels_data['widgets']) ) {
+ foreach ( $panels_data['widgets'] as $i => $widget ) {
+ if( empty( $panels_data['widgets'][$i]['panels_info'] ) ) {
+ $panels_data['widgets'][$i]['panels_info'] = $panels_data['widgets'][$i]['info'];
+ unset($panels_data['widgets'][$i]['info']);
+ }
+ }
+ }
+
+ if( !empty( $panels_data['widgets'] ) && is_array($panels_data['widgets']) ){
+ foreach ( $panels_data['widgets'] as $widget ) {
+ // Put the widgets in the grids
+ $grids[ intval( $widget['panels_info']['grid']) ][ intval( $widget['panels_info']['cell'] ) ][] = $widget;
+ }
+ }
+
+ ob_start();
+
+ // Add the panel layout wrapper
+ echo '
',
+ 'widget_id' => 'widget-' . $grid . '-' . $cell . '-' . $panel
+ );
+
+ // If there is a style wrapper, add it.
+ if( !empty($style_wrapper) ) {
+ $args['before_widget'] = $args['before_widget'] . $style_wrapper;
+ $args['after_widget'] = '
' . $args['after_widget'];
+ }
+
+ if ( !empty($the_widget) && is_a($the_widget, 'WP_Widget') ) {
+ $the_widget->widget($args , $instance );
+ }
+ else {
+ // This gives themes a chance to display some sort of placeholder for missing widgets
+ echo apply_filters('siteorigin_panels_missing_widget', '', $widget, $args , $instance);
+ }
+}
+
+/**
+ * Add the Edit Home Page item to the admin bar.
+ *
+ * @param WP_Admin_Bar $admin_bar
+ * @return WP_Admin_Bar
+ */
+function siteorigin_panels_admin_bar_menu($admin_bar){
+ // Ignore this unless the theme is using the home page feature.
+ if( !siteorigin_panels_setting('home-page') ) return $admin_bar;
+ if( !current_user_can('edit_theme_options') ) return $admin_bar;
+
+ if( is_home() || is_front_page() ) {
+ if( ( is_page() && get_the_ID() == get_option('siteorigin_panels_home_page_id') ) || current_user_can('edit_theme_options') ) {
+ $admin_bar->add_node( array(
+ 'id' => 'edit-home-page',
+ 'title' => __('Edit Home Page', 'siteorigin-panels'),
+ 'href' => admin_url('themes.php?page=so_panels_home_page')
+ ) );
+ }
+
+ if( is_page() && get_the_ID() == get_option('siteorigin_panels_home_page_id') ) {
+ $admin_bar->remove_node('edit');
+ }
+ }
+
+ return $admin_bar;
+}
+add_action('admin_bar_menu', 'siteorigin_panels_admin_bar_menu', 100);
+
+/**
+ * Handles creating the preview.
+ */
+function siteorigin_panels_preview(){
+ if(isset($_GET['siteorigin_panels_preview']) && isset($_GET['_wpnonce']) && wp_verify_nonce($_GET['_wpnonce'], 'siteorigin-panels-preview')){
+ global $siteorigin_panels_is_preview;
+ $siteorigin_panels_is_preview = true;
+ // Set the panels home state to true
+ if(empty($_POST['post_id'])) $GLOBALS['siteorigin_panels_is_panels_home'] = true;
+ add_action('siteorigin_panels_data', 'siteorigin_panels_home_preview_load_data');
+ locate_template( siteorigin_panels_setting('home-template'), true );
+ exit();
+ }
+}
+add_action('template_redirect', 'siteorigin_panels_preview');
+
+/**
+ * Is this a preview.
+ *
+ * @return bool
+ */
+function siteorigin_panels_is_preview(){
+ global $siteorigin_panels_is_preview;
+ return (bool) $siteorigin_panels_is_preview;
+}
+
+/**
+ * Hide the admin bar for panels previews.
+ *
+ * @param $show
+ * @return bool
+ */
+function siteorigin_panels_preview_adminbar($show){
+ if(!$show) return false;
+ return !(isset($_GET['siteorigin_panels_preview']) && wp_verify_nonce($_GET['_wpnonce'], 'siteorigin-panels-preview'));
+}
+add_filter('show_admin_bar', 'siteorigin_panels_preview_adminbar');
+
+/**
+ * This is a way to show previews of panels, especially for the home page.
+ *
+ * @param $val
+ * @return array
+ */
+function siteorigin_panels_home_preview_load_data($val){
+ if( isset($_GET['siteorigin_panels_preview']) ){
+ $val = siteorigin_panels_get_panels_data_from_post( $_POST );
+ }
+
+ return $val;
+}
+
+/**
+ * Add all the necessary body classes.
+ *
+ * @param $classes
+ * @return array
+ */
+function siteorigin_panels_body_class($classes){
+ if( siteorigin_panels_is_panel() ) $classes[] = 'siteorigin-panels';
+ if( siteorigin_panels_is_home() ) $classes[] = 'siteorigin-panels-home';
+
+ if(isset($_GET['siteorigin_panels_preview']) && isset($_GET['_wpnonce']) && wp_verify_nonce($_GET['_wpnonce'], 'siteorigin-panels-preview')) {
+ // This is a home page preview
+ $classes[] = 'siteorigin-panels';
+ $classes[] = 'siteorigin-panels-home';
+ }
+
+ return $classes;
+}
+add_filter('body_class', 'siteorigin_panels_body_class');
+
+/**
+ * Enqueue the required styles
+ */
+function siteorigin_panels_enqueue_styles(){
+ wp_register_style('siteorigin-panels-front', plugin_dir_url(__FILE__) . 'css/front.css', array(), SITEORIGIN_PANELS_VERSION );
+}
+add_action('wp_enqueue_scripts', 'siteorigin_panels_enqueue_styles', 1);
+
+/**
+ * Add a filter to import panels_data meta key. This fixes serialized PHP.
+ */
+function siteorigin_panels_wp_import_post_meta($post_meta){
+ foreach($post_meta as $i => $meta) {
+ if($meta['key'] == 'panels_data') {
+ $value = $meta['value'];
+ $value = preg_replace("/[\r\n]/", "<< >>", $value);
+ $value = preg_replace('!s:(\d+):"(.*?)";!e', "'s:'.strlen('$2').':\"$2\";'", $value);
+ $value = unserialize($value);
+ $value = array_map('siteorigin_panels_wp_import_post_meta_map', $value);
+
+ $post_meta[$i]['value'] = $value;
+ }
+ }
+
+ return $post_meta;
+}
+add_filter('wp_import_post_meta', 'siteorigin_panels_wp_import_post_meta');
+
+/**
+ * A callback that replaces temporary break tag with actual line breaks.
+ *
+ * @param $val
+ * @return array|mixed
+ */
+function siteorigin_panels_wp_import_post_meta_map($val) {
+ if(is_string($val)) return str_replace('<< >>', "\n", $val);
+ else return array_map('siteorigin_panels_wp_import_post_meta_map', $val);
+}
+
+/**
+ * Render a widget form with all the Page Builder specific fields
+ *
+ * @param string $widget The class of the widget
+ * @param array $instance Widget values
+ * @param bool $raw
+ * @return mixed|string The form
+ */
+function siteorigin_panels_render_form($widget, $instance = array(), $raw = false){
+ global $wp_widget_factory;
+
+ // This is a chance for plugins to replace missing widgets
+ $the_widget = !empty($wp_widget_factory->widgets[$widget]) ? $wp_widget_factory->widgets[$widget] : false;
+ $the_widget = apply_filters( 'siteorigin_panels_widget_object', $the_widget, $widget );
+
+ if ( empty($the_widget) || !is_a( $the_widget, 'WP_Widget' ) ) {
+ $widgets = siteorigin_panels_get_widgets();
+
+ if( !empty($widgets[$widget]) && !empty( $widgets[$widget]['plugin'] ) ) {
+ // We know about this widget, show a form about installing it.
+ $install_url = siteorigin_panels_plugin_activation_install_url($widgets[$widget]['plugin']['slug'], $widgets[$widget]['plugin']['name']);
+ $form =
+ '
' .
+ '
' . sprintf(
+ __("You need to install %s to use the widget %s. It's a free plugin available off the official WordPress plugin directory.", 'siteorigin-panels'),
+ $install_url,
+ $widgets[$widget]['plugin']['name'],
+ $widget
+ ). '
' .
+ '
' . __("Save and reload this page to start using the widget after you've installed it.") . '
' .
+ '
';
+ }
+ else {
+ // This widget is missing, so show a missing widgets form.
+ $form =
+ '
' .
+ sprintf(
+ __('The widget %s is not available. Please try locate and install the missing plugin. Post on the support forums if you need help.', 'siteorigin-panels'),
+ $widget,
+ 'http://siteorigin.com/thread/'
+ ).
+ '
';
+ }
+
+ // Allow other themes and plugins to change the missing widget form
+ return apply_filters('siteorigin_panels_missing_widget_form', $form, $widget, $instance);
+ }
+
+ if( $raw ) $instance = $the_widget->update($instance, $instance);
+
+ $the_widget->id = 'temp';
+ $the_widget->number = '{$id}';
+
+ ob_start();
+ $the_widget->form($instance);
+ $form = ob_get_clean();
+
+ // Convert the widget field naming into ones that Page Builder uses
+ $exp = preg_quote( $the_widget->get_field_name('____') );
+ $exp = str_replace('____', '(.*?)', $exp);
+ $form = preg_replace( '/'.$exp.'/', 'widgets[{$id}][$1]', $form );
+
+ $form = apply_filters('siteorigin_panels_widget_form', $form, $widget, $instance);
+
+ // Add all the information fields
+ return $form;
+}
+
+/**
+ * This takes existing Page Builder data and makes it RTL by reversing the content
+ */
+function siteorigin_panels_make_rtl($panels_data){
+
+ // To start, we need a cell count for every row
+ foreach($panels_data['widgets'] as &$widget) {
+ // This reverses the cells of the widgets
+ $count = $panels_data['grids'][ $widget['panels_info']['grid'] ]['cells'];
+ $widget['panels_info']['cell'] = abs( $widget['panels_info']['cell'] - $count + 1 );
+ }
+
+ // Now we need to swap around the grid cells because we're going to use float right instead.
+ $grid_cells = array();
+ foreach( $panels_data['grid_cells'] as $cell) {
+ if( empty( $grid_cells[ $cell['grid'] ] ) ) $grid_cells[ $cell['grid'] ] = array();
+ array_unshift( $grid_cells[ $cell['grid'] ], $cell );
+ }
+ $new_grid_cells = array();
+ foreach( $grid_cells as $i => $cells ) {
+ foreach($cells as $cell) {
+ $new_grid_cells[] = $cell;
+ }
+ }
+ $panels_data['grid_cells'] = $new_grid_cells;
+
+ return $panels_data;
+}
+
+/**
+ * Add action links to the plugin list for Page Builder.
+ *
+ * @param $links
+ * @return array
+ */
+function siteorigin_panels_plugin_action_links($links) {
+ $links[] = '' . __('Support Forum', 'siteorigin-panels') . '';
+ $links[] = '' . __('Newsletter', 'siteorigin-panels') . '';
+ return $links;
+}
+add_action('plugin_action_links_' . plugin_basename(__FILE__), 'siteorigin_panels_plugin_action_links');
+
+// Include the live editor file if we're in live editor mode.
+if( !empty( $_GET['siteorigin_panels_live_editor'] ) ) require_once plugin_dir_path(__FILE__) . 'inc/live-editor.php';
\ No newline at end of file
diff --git a/tpl/admin-home-page.php b/tpl/admin-home-page.php
new file mode 100644
index 000000000..f4253c650
--- /dev/null
+++ b/tpl/admin-home-page.php
@@ -0,0 +1,39 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tpl/help.php b/tpl/help.php
new file mode 100644
index 000000000..3cdfe6b6a
--- /dev/null
+++ b/tpl/help.php
@@ -0,0 +1,15 @@
+
+
+
+
+
+ full documentation on SiteOrigin.", 'siteorigin-panels' ) . ' ' .
+ __( "Ask a question on our support forum if you need help and sign up to our newsletter to stay up to date with future developments.", 'siteorigin-panels' ),
+ 'http://siteorigin.com/page-builder/documentation/' ,
+ 'http://siteorigin.com/threads/plugin-page-builder/',
+ 'http://siteorigin.com/#newsletter'
+ );
+ ?>
+
\ No newline at end of file
diff --git a/tpl/js-templates.php b/tpl/js-templates.php
new file mode 100644
index 000000000..4d879d1a7
--- /dev/null
+++ b/tpl/js-templates.php
@@ -0,0 +1,435 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tpl/metabox-panels.php b/tpl/metabox-panels.php
new file mode 100644
index 000000000..9fe115e2e
--- /dev/null
+++ b/tpl/metabox-panels.php
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tpl/options.php b/tpl/options.php
new file mode 100644
index 000000000..60edd0c8f
--- /dev/null
+++ b/tpl/options.php
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video/jplayer/Jplayer.swf b/video/jplayer/Jplayer.swf
new file mode 100755
index 000000000..2121715c8
Binary files /dev/null and b/video/jplayer/Jplayer.swf differ
diff --git a/video/jplayer/jquery.jplayer.min.js b/video/jplayer/jquery.jplayer.min.js
new file mode 100755
index 000000000..796e970e2
--- /dev/null
+++ b/video/jplayer/jquery.jplayer.min.js
@@ -0,0 +1,114 @@
+/*
+ * jPlayer Plugin for jQuery JavaScript Library
+ * http://www.jplayer.org
+ *
+ * Copyright (c) 2009 - 2013 Happyworm Ltd
+ * Licensed under the MIT license.
+ * http://opensource.org/licenses/MIT
+ *
+ * Author: Mark J Panaghiston
+ * Version: 2.5.0
+ * Date: 7th November 2013
+ */
+
+(function(b,f){"function"===typeof define&&define.amd?define(["jquery"],f):b.jQuery?f(b.jQuery):f(b.Zepto)})(this,function(b,f){b.fn.jPlayer=function(a){var c="string"===typeof a,d=Array.prototype.slice.call(arguments,1),e=this;a=!c&&d.length?b.extend.apply(null,[!0,a].concat(d)):a;if(c&&"_"===a.charAt(0))return e;c?this.each(function(){var c=b(this).data("jPlayer"),h=c&&b.isFunction(c[a])?c[a].apply(c,d):c;if(h!==c&&h!==f)return e=h,!1}):this.each(function(){var c=b(this).data("jPlayer");c?c.option(a||
+{}):b(this).data("jPlayer",new b.jPlayer(a,this))});return e};b.jPlayer=function(a,c){if(arguments.length){this.element=b(c);this.options=b.extend(!0,{},this.options,a);var d=this;this.element.bind("remove.jPlayer",function(){d.destroy()});this._init()}};"function"!==typeof b.fn.stop&&(b.fn.stop=function(){});b.jPlayer.emulateMethods="load play pause";b.jPlayer.emulateStatus="src readyState networkState currentTime duration paused ended playbackRate";b.jPlayer.emulateOptions="muted volume";b.jPlayer.reservedEvent=
+"ready flashreset resize repeat error warning";b.jPlayer.event={};b.each("ready flashreset resize repeat click error warning loadstart progress suspend abort emptied stalled play pause loadedmetadata loadeddata waiting playing canplay canplaythrough seeking seeked timeupdate ended ratechange durationchange volumechange".split(" "),function(){b.jPlayer.event[this]="jPlayer_"+this});b.jPlayer.htmlEvent="loadstart abort emptied stalled loadedmetadata loadeddata canplay canplaythrough".split(" ");b.jPlayer.pause=
+function(){b.each(b.jPlayer.prototype.instances,function(a,c){c.data("jPlayer").status.srcSet&&c.jPlayer("pause")})};b.jPlayer.timeFormat={showHour:!1,showMin:!0,showSec:!0,padHour:!1,padMin:!0,padSec:!0,sepHour:":",sepMin:":",sepSec:""};var m=function(){this.init()};m.prototype={init:function(){this.options={timeFormat:b.jPlayer.timeFormat}},time:function(a){var c=new Date(1E3*(a&&"number"===typeof a?a:0)),b=c.getUTCHours();a=this.options.timeFormat.showHour?c.getUTCMinutes():c.getUTCMinutes()+60*
+b;c=this.options.timeFormat.showMin?c.getUTCSeconds():c.getUTCSeconds()+60*a;b=this.options.timeFormat.padHour&&10>b?"0"+b:b;a=this.options.timeFormat.padMin&&10>a?"0"+a:a;c=this.options.timeFormat.padSec&&10>c?"0"+c:c;b=""+(this.options.timeFormat.showHour?b+this.options.timeFormat.sepHour:"");b+=this.options.timeFormat.showMin?a+this.options.timeFormat.sepMin:"";return b+=this.options.timeFormat.showSec?c+this.options.timeFormat.sepSec:""}};var n=new m;b.jPlayer.convertTime=function(a){return n.time(a)};
+b.jPlayer.uaBrowser=function(a){a=a.toLowerCase();var c=/(opera)(?:.*version)?[ \/]([\w.]+)/,b=/(msie) ([\w.]+)/,e=/(mozilla)(?:.*? rv:([\w.]+))?/;a=/(webkit)[ \/]([\w.]+)/.exec(a)||c.exec(a)||b.exec(a)||0>a.indexOf("compatible")&&e.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}};b.jPlayer.uaPlatform=function(a){var c=a.toLowerCase(),b=/(android)/,e=/(mobile)/;a=/(ipad|iphone|ipod|android|blackberry|playbook|windows ce|webos)/.exec(c)||[];c=/(ipad|playbook)/.exec(c)||!e.exec(c)&&b.exec(c)||
+[];a[1]&&(a[1]=a[1].replace(/\s/g,"_"));return{platform:a[1]||"",tablet:c[1]||""}};b.jPlayer.browser={};b.jPlayer.platform={};var k=b.jPlayer.uaBrowser(navigator.userAgent);k.browser&&(b.jPlayer.browser[k.browser]=!0,b.jPlayer.browser.version=k.version);k=b.jPlayer.uaPlatform(navigator.userAgent);k.platform&&(b.jPlayer.platform[k.platform]=!0,b.jPlayer.platform.mobile=!k.tablet,b.jPlayer.platform.tablet=!!k.tablet);b.jPlayer.getDocMode=function(){var a;b.jPlayer.browser.msie&&(document.documentMode?
+a=document.documentMode:(a=5,document.compatMode&&"CSS1Compat"===document.compatMode&&(a=7)));return a};b.jPlayer.browser.documentMode=b.jPlayer.getDocMode();b.jPlayer.nativeFeatures={init:function(){var a=document,c=a.createElement("video"),b={w3c:"fullscreenEnabled fullscreenElement requestFullscreen exitFullscreen fullscreenchange fullscreenerror".split(" "),moz:"mozFullScreenEnabled mozFullScreenElement mozRequestFullScreen mozCancelFullScreen mozfullscreenchange mozfullscreenerror".split(" "),
+webkit:" webkitCurrentFullScreenElement webkitRequestFullScreen webkitCancelFullScreen webkitfullscreenchange ".split(" "),webkitVideo:"webkitSupportsFullscreen webkitDisplayingFullscreen webkitEnterFullscreen webkitExitFullscreen ".split(" ")},e=["w3c","moz","webkit","webkitVideo"],g,h;this.fullscreen=c={support:{w3c:!!a[b.w3c[0]],moz:!!a[b.moz[0]],webkit:"function"===typeof a[b.webkit[3]],webkitVideo:"function"===typeof c[b.webkitVideo[2]]},used:{}};g=0;for(h=e.length;gNumber(b.jPlayer.browser.version)||9>b.jPlayer.browser.documentMode)){d=['','','','',''];c=document.createElement('');
+for(var e=0;e").join(">").split('"').join(""")},_qualifyURL:function(a){var c=document.createElement("div");c.innerHTML='x';return c.firstChild.href},_absoluteMediaUrls:function(a){var c=this;b.each(a,function(b,e){c.format[b]&&
+(a[b]=c._qualifyURL(e))});return a},setMedia:function(a){var c=this,d=!1,e=this.status.media.poster!==a.poster;this._resetMedia();this._resetGate();this._resetActive();a=this._absoluteMediaUrls(a);b.each(this.formats,function(e,f){var k="video"===c.format[f].media;b.each(c.solutions,function(b,e){if(c[e].support[f]&&c._validString(a[f])){var g="html"===e;k?(g?(c.html.video.gate=!0,c._html_setVideo(a),c.html.active=!0):(c.flash.gate=!0,c._flash_setVideo(a),c.flash.active=!0),c.css.jq.videoPlay.length&&
+c.css.jq.videoPlay.show(),c.status.video=!0):(g?(c.html.audio.gate=!0,c._html_setAudio(a),c.html.active=!0):(c.flash.gate=!0,c._flash_setAudio(a),c.flash.active=!0),c.css.jq.videoPlay.length&&c.css.jq.videoPlay.hide(),c.status.video=!1);d=!0;return!1}});if(d)return!1});d?(this.status.nativeVideoControls&&this.html.video.gate||!this._validString(a.poster)||(e?this.htmlElement.poster.src=a.poster:this.internal.poster.jq.show()),this.status.srcSet=!0,this.status.media=b.extend({},a),this._updateButtons(!1),
+this._updateInterface()):this._error({type:b.jPlayer.error.NO_SUPPORT,context:"{supplied:'"+this.options.supplied+"'}",message:b.jPlayer.errorMsg.NO_SUPPORT,hint:b.jPlayer.errorHint.NO_SUPPORT})},_resetMedia:function(){this._resetStatus();this._updateButtons(!1);this._updateInterface();this._seeked();this.internal.poster.jq.hide();clearTimeout(this.internal.htmlDlyCmdId);this.html.active?this._html_resetMedia():this.flash.active&&this._flash_resetMedia()},clearMedia:function(){this._resetMedia();
+this.html.active?this._html_clearMedia():this.flash.active&&this._flash_clearMedia();this._resetGate();this._resetActive()},load:function(){this.status.srcSet?this.html.active?this._html_load():this.flash.active&&this._flash_load():this._urlNotSetError("load")},focus:function(){this.options.keyEnabled&&(b.jPlayer.focus=this)},play:function(a){a="number"===typeof a?a:NaN;this.status.srcSet?(this.focus(),this.html.active?this._html_play(a):this.flash.active&&this._flash_play(a)):this._urlNotSetError("play")},
+videoPlay:function(){this.play()},pause:function(a){a="number"===typeof a?a:NaN;this.status.srcSet?this.html.active?this._html_pause(a):this.flash.active&&this._flash_pause(a):this._urlNotSetError("pause")},tellOthers:function(a,c){var d=this,e="function"===typeof c,g=Array.prototype.slice.call(arguments);"string"===typeof a&&(e&&g.splice(1,1),b.each(this.instances,function(){d.element!==this&&(e&&!c.call(this.data("jPlayer"),d)||this.jPlayer.apply(this,g))}))},pauseOthers:function(a){this.tellOthers("pause",
+function(){return this.status.srcSet},a)},stop:function(){this.status.srcSet?this.html.active?this._html_pause(0):this.flash.active&&this._flash_pause(0):this._urlNotSetError("stop")},playHead:function(a){a=this._limitValue(a,0,100);this.status.srcSet?this.html.active?this._html_playHead(a):this.flash.active&&this._flash_playHead(a):this._urlNotSetError("playHead")},_muted:function(a){this.mutedWorker(a);this.options.globalVolume&&this.tellOthers("mutedWorker",function(){return this.options.globalVolume},
+a)},mutedWorker:function(a){this.options.muted=a;this.html.used&&this._html_setProperty("muted",a);this.flash.used&&this._flash_mute(a);this.html.video.gate||this.html.audio.gate||(this._updateMute(a),this._updateVolume(this.options.volume),this._trigger(b.jPlayer.event.volumechange))},mute:function(a){a=a===f?!0:!!a;this._muted(a)},unmute:function(a){a=a===f?!0:!!a;this._muted(!a)},_updateMute:function(a){a===f&&(a=this.options.muted);this.css.jq.mute.length&&this.css.jq.unmute.length&&(this.status.noVolume?
+(this.css.jq.mute.hide(),this.css.jq.unmute.hide()):a?(this.css.jq.mute.hide(),this.css.jq.unmute.show()):(this.css.jq.mute.show(),this.css.jq.unmute.hide()))},volume:function(a){this.volumeWorker(a);this.options.globalVolume&&this.tellOthers("volumeWorker",function(){return this.options.globalVolume},a)},volumeWorker:function(a){a=this._limitValue(a,0,1);this.options.volume=a;this.html.used&&this._html_setProperty("volume",a);this.flash.used&&this._flash_volume(a);this.html.video.gate||this.html.audio.gate||
+(this._updateVolume(a),this._trigger(b.jPlayer.event.volumechange))},volumeBar:function(a){if(this.css.jq.volumeBar.length){var c=b(a.currentTarget),d=c.offset(),e=a.pageX-d.left,g=c.width();a=c.height()-a.pageY+d.top;c=c.height();this.options.verticalVolume?this.volume(a/c):this.volume(e/g)}this.options.muted&&this._muted(!1)},volumeBarValue:function(){},_updateVolume:function(a){a===f&&(a=this.options.volume);a=this.options.muted?0:a;this.status.noVolume?(this.css.jq.volumeBar.length&&this.css.jq.volumeBar.hide(),
+this.css.jq.volumeBarValue.length&&this.css.jq.volumeBarValue.hide(),this.css.jq.volumeMax.length&&this.css.jq.volumeMax.hide()):(this.css.jq.volumeBar.length&&this.css.jq.volumeBar.show(),this.css.jq.volumeBarValue.length&&(this.css.jq.volumeBarValue.show(),this.css.jq.volumeBarValue[this.options.verticalVolume?"height":"width"](100*a+"%")),this.css.jq.volumeMax.length&&this.css.jq.volumeMax.show())},volumeMax:function(){this.volume(1);this.options.muted&&this._muted(!1)},_cssSelectorAncestor:function(a){var c=
+this;this.options.cssSelectorAncestor=a;this._removeUiClass();this.ancestorJq=a?b(a):[];a&&1!==this.ancestorJq.length&&this._warning({type:b.jPlayer.warning.CSS_SELECTOR_COUNT,context:a,message:b.jPlayer.warningMsg.CSS_SELECTOR_COUNT+this.ancestorJq.length+" found for cssSelectorAncestor.",hint:b.jPlayer.warningHint.CSS_SELECTOR_COUNT});this._addUiClass();b.each(this.options.cssSelector,function(a,b){c._cssSelector(a,b)});this._updateInterface();this._updateButtons();this._updateAutohide();this._updateVolume();
+this._updateMute()},_cssSelector:function(a,c){var d=this;"string"===typeof c?b.jPlayer.prototype.options.cssSelector[a]?(this.css.jq[a]&&this.css.jq[a].length&&this.css.jq[a].unbind(".jPlayer"),this.options.cssSelector[a]=c,this.css.cs[a]=this.options.cssSelectorAncestor+" "+c,this.css.jq[a]=c?b(this.css.cs[a]):[],this.css.jq[a].length&&this.css.jq[a].bind("click.jPlayer",function(c){c.preventDefault();d[a](c);b(this).blur()}),c&&1!==this.css.jq[a].length&&this._warning({type:b.jPlayer.warning.CSS_SELECTOR_COUNT,
+context:this.css.cs[a],message:b.jPlayer.warningMsg.CSS_SELECTOR_COUNT+this.css.jq[a].length+" found for "+a+" method.",hint:b.jPlayer.warningHint.CSS_SELECTOR_COUNT})):this._warning({type:b.jPlayer.warning.CSS_SELECTOR_METHOD,context:a,message:b.jPlayer.warningMsg.CSS_SELECTOR_METHOD,hint:b.jPlayer.warningHint.CSS_SELECTOR_METHOD}):this._warning({type:b.jPlayer.warning.CSS_SELECTOR_STRING,context:c,message:b.jPlayer.warningMsg.CSS_SELECTOR_STRING,hint:b.jPlayer.warningHint.CSS_SELECTOR_STRING})},
+seekBar:function(a){if(this.css.jq.seekBar.length){var c=b(a.currentTarget),d=c.offset();a=a.pageX-d.left;c=c.width();this.playHead(100*a/c)}},playBar:function(){},playbackRate:function(a){this._setOption("playbackRate",a)},playbackRateBar:function(a){if(this.css.jq.playbackRateBar.length){var c=b(a.currentTarget),d=c.offset(),e=a.pageX-d.left,g=c.width();a=c.height()-a.pageY+d.top;c=c.height();this.playbackRate((this.options.verticalPlaybackRate?a/c:e/g)*(this.options.maxPlaybackRate-this.options.minPlaybackRate)+
+this.options.minPlaybackRate)}},playbackRateBarValue:function(){},_updatePlaybackRate:function(){var a=(this.options.playbackRate-this.options.minPlaybackRate)/(this.options.maxPlaybackRate-this.options.minPlaybackRate);this.status.playbackRateEnabled?(this.css.jq.playbackRateBar.length&&this.css.jq.playbackRateBar.show(),this.css.jq.playbackRateBarValue.length&&(this.css.jq.playbackRateBarValue.show(),this.css.jq.playbackRateBarValue[this.options.verticalPlaybackRate?"height":"width"](100*a+"%"))):
+(this.css.jq.playbackRateBar.length&&this.css.jq.playbackRateBar.hide(),this.css.jq.playbackRateBarValue.length&&this.css.jq.playbackRateBarValue.hide())},repeat:function(){this._loop(!0)},repeatOff:function(){this._loop(!1)},_loop:function(a){this.options.loop!==a&&(this.options.loop=a,this._updateButtons(),this._trigger(b.jPlayer.event.repeat))},currentTime:function(){},duration:function(){},gui:function(){},noSolution:function(){},option:function(a,c){var d=a;if(0===arguments.length)return b.extend(!0,
+{},this.options);if("string"===typeof a){var e=a.split(".");if(c===f){for(var d=b.extend(!0,{},this.options),g=0;g=
+a&&(b=!0);return b},_validString:function(a){return a&&"string"===typeof a},_limitValue:function(a,b,d){return ad?d:a},_urlNotSetError:function(a){this._error({type:b.jPlayer.error.URL_NOT_SET,context:a,message:b.jPlayer.errorMsg.URL_NOT_SET,hint:b.jPlayer.errorHint.URL_NOT_SET})},_flashError:function(a){var c;c=this.internal.ready?"FLASH_DISABLED":"FLASH";this._error({type:b.jPlayer.error[c],context:this.internal.flash.swf,message:b.jPlayer.errorMsg[c]+a.message,hint:b.jPlayer.errorHint[c]});
+this.internal.flash.jq.css({width:"1px",height:"1px"})},_error:function(a){this._trigger(b.jPlayer.event.error,a);this.options.errorAlerts&&this._alert("Error!"+(a.message?"\n"+a.message:"")+(a.hint?"\n"+a.hint:"")+"\nContext: "+a.context)},_warning:function(a){this._trigger(b.jPlayer.event.warning,f,a);this.options.warningAlerts&&this._alert("Warning!"+(a.message?"\n"+a.message:"")+(a.hint?"\n"+a.hint:"")+"\nContext: "+a.context)},_alert:function(a){a="jPlayer "+this.version.script+" : id='"+this.internal.self.id+
+"' : "+a;this.options.consoleAlerts?console&&console.log&&console.log(a):alert(a)},_emulateHtmlBridge:function(){var a=this;b.each(b.jPlayer.emulateMethods.split(/\s+/g),function(b,d){a.internal.domNode[d]=function(b){a[d](b)}});b.each(b.jPlayer.event,function(c,d){var e=!0;b.each(b.jPlayer.reservedEvent.split(/\s+/g),function(a,b){if(b===c)return e=!1});e&&a.element.bind(d+".jPlayer.jPlayerHtml",function(){a._emulateHtmlUpdate();var b=document.createEvent("Event");b.initEvent(c,!1,!0);a.internal.domNode.dispatchEvent(b)})})},
+_emulateHtmlUpdate:function(){var a=this;b.each(b.jPlayer.emulateStatus.split(/\s+/g),function(b,d){a.internal.domNode[d]=a.status[d]});b.each(b.jPlayer.emulateOptions.split(/\s+/g),function(b,d){a.internal.domNode[d]=a.options[d]})},_destroyHtmlBridge:function(){var a=this;this.element.unbind(".jPlayerHtml");b.each((b.jPlayer.emulateMethods+" "+b.jPlayer.emulateStatus+" "+b.jPlayer.emulateOptions).split(/\s+/g),function(b,d){delete a.internal.domNode[d]})}};b.jPlayer.error={FLASH:"e_flash",FLASH_DISABLED:"e_flash_disabled",
+NO_SOLUTION:"e_no_solution",NO_SUPPORT:"e_no_support",URL:"e_url",URL_NOT_SET:"e_url_not_set",VERSION:"e_version"};b.jPlayer.errorMsg={FLASH:"jPlayer's Flash fallback is not configured correctly, or a command was issued before the jPlayer Ready event. Details: ",FLASH_DISABLED:"jPlayer's Flash fallback has been disabled by the browser due to the CSS rules you have used. Details: ",NO_SOLUTION:"No solution can be found by jPlayer in this browser. Neither HTML nor Flash can be used.",NO_SUPPORT:"It is not possible to play any media format provided in setMedia() on this browser using your current options.",
+URL:"Media URL could not be loaded.",URL_NOT_SET:"Attempt to issue media playback commands, while no media url is set.",VERSION:"jPlayer "+b.jPlayer.prototype.version.script+" needs Jplayer.swf version "+b.jPlayer.prototype.version.needFlash+" but found "};b.jPlayer.errorHint={FLASH:"Check your swfPath option and that Jplayer.swf is there.",FLASH_DISABLED:"Check that you have not display:none; the jPlayer entity or any ancestor.",NO_SOLUTION:"Review the jPlayer options: support and supplied.",NO_SUPPORT:"Video or audio formats defined in the supplied option are missing.",
+URL:"Check media URL is valid.",URL_NOT_SET:"Use setMedia() to set the media URL.",VERSION:"Update jPlayer files."};b.jPlayer.warning={CSS_SELECTOR_COUNT:"e_css_selector_count",CSS_SELECTOR_METHOD:"e_css_selector_method",CSS_SELECTOR_STRING:"e_css_selector_string",OPTION_KEY:"e_option_key"};b.jPlayer.warningMsg={CSS_SELECTOR_COUNT:"The number of css selectors found did not equal one: ",CSS_SELECTOR_METHOD:"The methodName given in jPlayer('cssSelector') is not a valid jPlayer method.",CSS_SELECTOR_STRING:"The methodCssSelector given in jPlayer('cssSelector') is not a String or is empty.",
+OPTION_KEY:"The option requested in jPlayer('option') is undefined."};b.jPlayer.warningHint={CSS_SELECTOR_COUNT:"Check your css selector and the ancestor.",CSS_SELECTOR_METHOD:"Check your method name.",CSS_SELECTOR_STRING:"Check your css selector is a string.",OPTION_KEY:"Check your option name."}});
\ No newline at end of file
diff --git a/video/jplayer/skins/premium/gui.php b/video/jplayer/skins/premium/gui.php
new file mode 100644
index 000000000..6aa8037db
--- /dev/null
+++ b/video/jplayer/skins/premium/gui.php
@@ -0,0 +1,32 @@
+