/wp-content/plugins/siteorigin-panels/js/siteorigin-panels.js
JavaScript | 1524 lines | 895 code | 261 blank | 368 comment | 72 complexity | 0b760e1f33fede61690119a197f1b96f MD5 | raw file
- /**
- * Everything we need for SiteOrigin Page Builder.
- *
- * @copyright Greg Priday 2013 - 2014 - <https://siteorigin.com/>
- * @license GPL 3.0 http://www.gnu.org/licenses/gpl.html
- */
- /**
- * Convert template into something compatible with Underscore.js templates
- *
- * @param s
- * @return {*}
- */
- String.prototype.panelsProcessTemplate = function(){
- var s = this;
- s = s.replace(/{{%/g, '<%');
- s = s.replace(/%}}/g, '%>');
- s = s.trim();
- return s;
- };
- ( 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: {}
- },
- /**
- * @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 false; }
- 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().panelsProcessTemplate() ),
- // 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().panelsProcessTemplate() ),
- 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');
- // 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(),
- 'z-index' : 10000,
- 'position' :'fixed'
- })
- .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
- */
- 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,
- /**
- * Destroy all the rows in this collection
- */
- empty: function(){
- var model;
- do {
- model = this.collection.first();
- if( !model ) { break; }
- model.destroy();
- } while( true );
- }
- } );
- /**
- * View for handling the row.
- */
- panels.view.row = Backbone.View.extend( {
- template: _.template( $('#siteorigin-panels-builder-row').html().panelsProcessTemplate() ),
- 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 false;
- }
- // 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(
- '<span class="dashicons dashicons-yes"></span>' + 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 false;
- }
- 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 newRow = builderModel.addRow( row, { noAnimate: true } );
- if( typeof data.grids[i].style !== 'undefined' ) {
- newRow.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) {
- }
- } );
- },
- /**
- * 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().panelsProcessTemplate() ),
- 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(
- $( '<a id="content-panels" class="hide-if-no-js wp-switch-editor switch-panels">' + metabox.find( 'h3.hndle span' ).html() + '</a>' )
- .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');
- } )
- );
- // WordPress 4.1 changed the float of the tabs. Reorder them here.
- // After WP 4.3 is released we'll make the new ordering default
- if( $('body').hasClass('branch-4-1') || $('body').hasClass('branch-4-2') ) {
- $( '#wp-content-wrap .wp-editor-tabs #content-panels' )
- .appendTo( $( '#wp-content-wrap .wp-editor-tabs' ) );
- }
- // 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'