PageRenderTime 63ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/wp-content/plugins/siteorigin-panels/js/siteorigin-panels.js

https://gitlab.com/haque.mdmanzurul/wp-harpar-carolyn
JavaScript | 1524 lines | 895 code | 261 blank | 368 comment | 72 complexity | 0b760e1f33fede61690119a197f1b96f MD5 | raw file
  1. /**
  2. * Everything we need for SiteOrigin Page Builder.
  3. *
  4. * @copyright Greg Priday 2013 - 2014 - <https://siteorigin.com/>
  5. * @license GPL 3.0 http://www.gnu.org/licenses/gpl.html
  6. */
  7. /**
  8. * Convert template into something compatible with Underscore.js templates
  9. *
  10. * @param s
  11. * @return {*}
  12. */
  13. String.prototype.panelsProcessTemplate = function(){
  14. var s = this;
  15. s = s.replace(/{{%/g, '<%');
  16. s = s.replace(/%}}/g, '%>');
  17. s = s.trim();
  18. return s;
  19. };
  20. ( function( $, _, panelsOptions ){
  21. var panels = {
  22. model : { },
  23. collection : { },
  24. view : { },
  25. dialog : { },
  26. fn : {}
  27. };
  28. /**
  29. * Model for an instance of a widget
  30. */
  31. panels.model.widget = Backbone.Model.extend( {
  32. cell: null,
  33. defaults : {
  34. // The PHP Class of the widget
  35. class : null,
  36. // Is this class missing?
  37. missing : false,
  38. // The values of the widget
  39. values: {},
  40. // Have the current values been passed through the widgets update function
  41. raw: false,
  42. // Visual style fields
  43. styles: {}
  44. },
  45. /**
  46. * @param field
  47. * @returns {*}
  48. */
  49. getWidgetField: function(field) {
  50. if(typeof panelsOptions.widgets[ this.get('class') ] === 'undefined') {
  51. if(field === 'title' || field === 'description') {
  52. return panelsOptions.loc.missing_widget[field];
  53. }
  54. else {
  55. return '';
  56. }
  57. }
  58. else {
  59. return panelsOptions.widgets[this.get('class')][field];
  60. }
  61. },
  62. /**
  63. * Move this widget to a new cell
  64. *
  65. * @param panels.model.cell newCell
  66. */
  67. moveToCell: function(newCell){
  68. if( this.cell.cid === newCell.cid ) { return false; }
  69. this.cell = newCell;
  70. this.collection.remove(this, {silent:true});
  71. newCell.widgets.add(this, {silent:true});
  72. },
  73. /**
  74. * Trigger an event on the model that indicates a user wants to edit it
  75. */
  76. triggerEdit: function(){
  77. this.trigger('user_edit', this);
  78. },
  79. /**
  80. * Trigger an event on the widget that indicates a user wants to duplicate it
  81. */
  82. triggerDuplicate: function(){
  83. this.trigger('user_duplicate', this);
  84. },
  85. /**
  86. * This is basically a wrapper for set that checks if we need to trigger a change
  87. */
  88. setValues: function(values){
  89. var hasChanged = false;
  90. if( JSON.stringify( values ) !== JSON.stringify( this.get('values') ) ) {
  91. hasChanged = true;
  92. }
  93. this.set( 'values', values, {silent: true} );
  94. if( hasChanged ) {
  95. // We'll trigger our own change events
  96. this.trigger('change');
  97. this.trigger('change:values');
  98. }
  99. },
  100. /**
  101. * Create a clone of this widget attached to the given cell.
  102. *
  103. * @param {panels.model.cell} cell
  104. * @returns {panels.model.widget}
  105. */
  106. clone: function( cell, options ){
  107. if( typeof cell === 'undefined' ) { cell = this.cell; }
  108. var clone = new this.constructor( this.attributes );
  109. // Create a deep clone of the original values
  110. var cloneValues = JSON.parse( JSON.stringify( this.get('values') ) );
  111. if( this.get('class') === "SiteOrigin_Panels_Widgets_Layout" ) {
  112. // Special case of this being a layout widget, it needs a new ID
  113. cloneValues.builder_id = Math.random().toString(36).substr(2);
  114. }
  115. clone.set( 'values', cloneValues, { silent: true } );
  116. clone.set( 'collection', cell.widgets, { silent: true } );
  117. clone.cell = cell;
  118. clone.isDuplicate = true;
  119. return clone;
  120. },
  121. /**
  122. * Gets the value that makes most sense as the title.
  123. */
  124. getTitle: function(){
  125. var widgetData = panelsOptions.widgets[this.get('class')];
  126. if( typeof widgetData.panels_title !== 'undefined' ) {
  127. // This means that the widget has told us which field it wants us to use as a title
  128. if( widgetData.panels_title === false ) {
  129. return panelsOptions.widgets[this.get('class')].description;
  130. }
  131. }
  132. var values = this.get('values');
  133. var thisModel = this;
  134. // Create a list of fields to check for a title
  135. var titleFields = ['title', 'text'];
  136. for (var k in values){
  137. titleFields.push( k );
  138. }
  139. titleFields = _.uniq(titleFields);
  140. for( var i in titleFields ) {
  141. if(
  142. typeof values[titleFields[i]] !== 'undefined' &&
  143. typeof values[titleFields[i]] === 'string' &&
  144. values[titleFields[i]] !== '' &&
  145. !$.isNumeric( values[titleFields[i]] )
  146. ) {
  147. var title = values[ titleFields[i] ];
  148. title = title.replace(/<\/?[^>]+(>|$)/g, "");
  149. var parts = title.split(" ");
  150. parts = parts.slice(0, 20);
  151. return parts.join(' ');
  152. }
  153. }
  154. // If we still have nothing, then just return the widget description
  155. return this.getWidgetField('description');
  156. }
  157. } );
  158. /**
  159. * The view for a widget in the builder interface
  160. */
  161. panels.view.widget = Backbone.View.extend({
  162. template: _.template( $('#siteorigin-panels-builder-widget').html().panelsProcessTemplate() ),
  163. // The cell view that
  164. cell: null,
  165. dialog: null,
  166. events: {
  167. 'click .widget-edit' : 'editHandler',
  168. 'click .title h4' : 'editHandler',
  169. 'click .actions .widget-duplicate' : 'duplicateHandler',
  170. 'click .actions .widget-delete' : 'deleteHandler'
  171. },
  172. /**
  173. * Initialize the widget
  174. */
  175. initialize: function(){
  176. // The 2 user actions on the model that this view will handle.
  177. this.model.on('user_edit', this.editHandler, this);
  178. this.model.on('user_duplicate', this.duplicateHandler, this);
  179. this.model.on('destroy', this.onModelDestroy, this);
  180. this.model.on('visual_destroy', this.visualDestroyModel, this);
  181. this.model.on('change:values', this.onModelChange, this);
  182. },
  183. /**
  184. * Render the widget
  185. */
  186. render: function(options){
  187. options = _.extend({'loadForm': false}, options);
  188. this.setElement( this.template( {
  189. title : this.model.getWidgetField('title'),
  190. description : this.model.getTitle()
  191. } ) );
  192. this.$el.data( 'view', this );
  193. if( _.size( this.model.get('values') ) === 0 || options.loadForm) {
  194. // If this widget doesn't have a value, create a form and save it
  195. var dialog = this.getEditDialog();
  196. // Save the widget as soon as the form is loaded
  197. dialog.once('form_loaded', dialog.saveWidget, dialog);
  198. // Setup the dialog to load the form
  199. dialog.setupDialog();
  200. }
  201. },
  202. /**
  203. * Display an animation that implies creation using a visual animation
  204. */
  205. visualCreate: function(){
  206. this.$el.hide().fadeIn( 'fast' );
  207. },
  208. /**
  209. * Get the dialog view of the form that edits this widget
  210. *
  211. * @returns {null}
  212. */
  213. getEditDialog: function(){
  214. if(this.dialog === null){
  215. this.dialog = new panels.dialog.widget({
  216. model: this.model
  217. });
  218. this.dialog.setBuilder(this.cell.row.builder);
  219. // Store the widget view
  220. this.dialog.widgetView = this;
  221. }
  222. return this.dialog;
  223. },
  224. /**
  225. * Handle clicking on edit widget.
  226. *
  227. * @returns {boolean}
  228. */
  229. editHandler: function(){
  230. // Create a new dialog for editing this
  231. this.getEditDialog().openDialog();
  232. return false;
  233. },
  234. /**
  235. * Handle clicking on duplicate.
  236. *
  237. * @returns {boolean}
  238. */
  239. duplicateHandler: function(){
  240. // Add the history entry
  241. this.cell.row.builder.addHistoryEntry('widget_duplicated');
  242. // Create the new widget and connect it to the widget collection for the current row
  243. var newWidget = this.model.clone( this.model.cell );
  244. this.cell.model.widgets.add(newWidget, {
  245. // Add this after the existing model
  246. at: this.model.collection.indexOf( this.model ) + 1
  247. });
  248. return false;
  249. },
  250. /**
  251. * Handle clicking on delete.
  252. *
  253. * @returns {boolean}
  254. */
  255. deleteHandler: function(){
  256. this.model.trigger('visual_destroy');
  257. return false;
  258. },
  259. onModelChange: function(){
  260. // Update the description when ever the model changes
  261. this.$('.description').html( this.model.getTitle() );
  262. },
  263. /**
  264. * When the model is destroyed, fade it out
  265. */
  266. onModelDestroy: function(){
  267. this.remove();
  268. },
  269. /**
  270. * Visually destroy a model
  271. */
  272. visualDestroyModel: function(){
  273. // Add the history entry
  274. this.cell.row.builder.addHistoryEntry('widget_deleted');
  275. var thisView = this;
  276. this.$el.fadeOut('fast', function(){
  277. thisView.cell.row.resize();
  278. thisView.model.destroy();
  279. } );
  280. }
  281. });
  282. /**
  283. * A collection of widgets, most often used for cells
  284. */
  285. panels.collection.widgets = Backbone.Collection.extend( {
  286. model : panels.model.widget,
  287. initialize: function(){
  288. }
  289. } );
  290. /**
  291. * A cell is a collection of widget instances
  292. */
  293. panels.model.cell = Backbone.Model.extend( {
  294. /* A collection of widgets */
  295. widgets: {},
  296. /* The row this model belongs to */
  297. row: null,
  298. defaults: {
  299. weight : 0
  300. },
  301. /**
  302. * Set up the cell model
  303. */
  304. initialize: function(){
  305. this.widgets = new panels.collection.widgets();
  306. this.on('destroy', this.onDestroy, this);
  307. },
  308. /**
  309. * Triggered when we destroy a cell
  310. */
  311. onDestroy: function(){
  312. _.invoke(this.widgets.toArray(), 'destroy');
  313. this.widgets.reset();
  314. },
  315. /**
  316. * Create a clone of the cell, along with all its widgets
  317. */
  318. clone: function(row, cloneOptions){
  319. if( typeof row === 'undefined' ) {
  320. row = this.row;
  321. }
  322. cloneOptions = _.extend({ cloneWidgets: true }, cloneOptions);
  323. var clone = new this.constructor( this.attributes );
  324. clone.set('collection', row.cells, {silent: true});
  325. clone.row = row;
  326. if( cloneOptions.cloneWidgets ) {
  327. // Now we're going add all the widgets that belong to this, to the clone
  328. this.widgets.each(function(widget){
  329. clone.widgets.add( widget.clone( clone, cloneOptions ), {silent: true} );
  330. });
  331. }
  332. return clone;
  333. }
  334. } );
  335. /**
  336. * A cell collection is used to represent a row
  337. */
  338. panels.collection.cells = Backbone.Collection.extend( {
  339. model: panels.cell,
  340. initialize: function(){
  341. this.on('add', this.onAddCell, this);
  342. },
  343. /**
  344. * Get the total weight for the cells in this collection.
  345. * @returns {number}
  346. */
  347. totalWeight: function(){
  348. var totalWeight = 0;
  349. this.each(function(cell){
  350. totalWeight += cell.get('weight');
  351. });
  352. return totalWeight;
  353. }
  354. } );
  355. /**
  356. * The view for a cell
  357. */
  358. panels.view.cell = Backbone.View.extend( {
  359. template: _.template( $('#siteorigin-panels-builder-cell').html().panelsProcessTemplate() ),
  360. events : {
  361. 'click .cell-wrapper' : 'handleCellClick',
  362. 'click .so-cell-actions a' : 'handleActionClick'
  363. },
  364. /* The row view that this cell is a part of */
  365. row: null,
  366. widgetSortable: null,
  367. initialize: function(){
  368. this.model.widgets.on('add', this.onAddWidget, this);
  369. },
  370. /**
  371. * Render the actual cell
  372. */
  373. render: function(){
  374. var templateArgs = {
  375. weight: this.model.get('weight'),
  376. totalWeight: this.row.model.cells.totalWeight()
  377. };
  378. this.setElement( this.template(templateArgs) );
  379. this.$el.data('view', this);
  380. // Now lets render any widgets that are currently in the row
  381. var thisView = this;
  382. this.model.widgets.each(function(widget){
  383. var widgetView = new panels.view.widget( { model: widget } );
  384. widgetView.cell = thisView;
  385. widgetView.render();
  386. widgetView.$el.appendTo( thisView.$('.widgets-container') );
  387. });
  388. this.initSortable();
  389. this.initResizable();
  390. },
  391. /**
  392. * Initialize the widget sortable
  393. */
  394. initSortable: function(){
  395. var cellView = this;
  396. var builderID = cellView.row.builder.$el.attr('id');
  397. // Create a widget sortable that's connected with all other cells
  398. this.widgetSortable = this.$el.find('.widgets-container').sortable( {
  399. placeholder: "so-widget-sortable-highlight",
  400. connectWith: '#' + builderID + ' .so-cells .cell .widgets-container',
  401. tolerance:'pointer',
  402. scroll: false,
  403. over: function(e, ui){
  404. // This will make all the rows in the current builder resize
  405. cellView.row.builder.trigger('widget_sortable_move');
  406. },
  407. stop: function(e, ui){
  408. cellView.row.builder.addHistoryEntry('widget_moved');
  409. var widget = $(ui.item).data('view');
  410. var targetCell = $(ui.item).closest('.cell').data('view');
  411. // Move the model and the view to the new cell
  412. widget.model.moveToCell( targetCell.model );
  413. widget.cell = targetCell;
  414. cellView.row.builder.sortCollections();
  415. },
  416. helper: function(e, el){
  417. var helper = el.clone()
  418. .css({
  419. 'width': el.outerWidth(),
  420. 'z-index' : 10000,
  421. 'position' :'fixed'
  422. })
  423. .addClass('widget-being-dragged').appendTo( 'body' );
  424. // Center the helper to the mouse cursor.
  425. if( el.outerWidth() > 720 ) {
  426. helper.animate({
  427. 'margin-left': e.pageX - el.offset().left - (480 / 2),
  428. 'width': 480
  429. }, 'fast');
  430. }
  431. return helper;
  432. }
  433. } );
  434. },
  435. /**
  436. * Refresh the widget sortable when a new widget is added
  437. */
  438. refreshSortable: function(){
  439. this.widgetSortable.sortable('refresh');
  440. },
  441. /**
  442. * This will make the cell resizble
  443. */
  444. initResizable: function(){
  445. // var neighbor = this.$el.previous().data('view');
  446. var handle = this.$('.resize-handle').css('position', 'absolute');
  447. var container = this.row.$el;
  448. var cellView = this;
  449. // The view of the cell to the left is stored when dragging starts.
  450. var previousCell;
  451. handle.draggable({
  452. axis: 'x',
  453. containment: container,
  454. start: function(e, ui){
  455. // Set the containment to the cell parent
  456. previousCell = cellView.$el.prev().data('view');
  457. if( typeof previousCell === 'undefined' ) { return false; }
  458. // Create the clone for the current cell
  459. var newCellClone = cellView.$el.clone().appendTo(ui.helper).css({
  460. position : 'absolute',
  461. top : '0',
  462. width : cellView.$el.outerWidth(),
  463. left : 5,
  464. height: cellView.$el.outerHeight()
  465. });
  466. newCellClone.find('.resize-handle').remove();
  467. // Create the clone for the previous cell
  468. var prevCellClone = previousCell.$el.clone().appendTo(ui.helper).css({
  469. position : 'absolute',
  470. top : '0',
  471. width : previousCell.$el.outerWidth(),
  472. right : 5,
  473. height: previousCell.$el.outerHeight()
  474. });
  475. prevCellClone.find('.resize-handle').remove();
  476. $(this).data({
  477. 'newCellClone' : newCellClone,
  478. 'prevCellClone' : prevCellClone
  479. });
  480. },
  481. drag: function(e, ui){
  482. // Calculate the new cell and previous cell widths as a percent
  483. var containerWidth = cellView.row.$el.width() + 10;
  484. var ncw = cellView.model.get('weight') - ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
  485. var pcw = previousCell.model.get('weight') + ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
  486. $(this).data('newCellClone').css('width', containerWidth * ncw )
  487. .find('.preview-cell-weight').html( Math.round(ncw*1000)/10 );
  488. $(this).data('prevCellClone').css('width', containerWidth * pcw )
  489. .find('.preview-cell-weight').html( Math.round(pcw*1000)/10 );
  490. },
  491. stop: function(e, ui){
  492. // Remove the clones
  493. $(this).data('newCellClone').remove();
  494. $(this).data('prevCellClone').remove();
  495. var containerWidth = cellView.row.$el.width() + 10;
  496. var ncw = cellView.model.get('weight') - ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
  497. var pcw = previousCell.model.get('weight') + ( ( ui.position.left + handle.outerWidth()/2 ) / containerWidth );
  498. if( ncw > 0.02 && pcw > 0.02 ) {
  499. cellView.row.builder.addHistoryEntry('cell_resized');
  500. cellView.model.set('weight', ncw);
  501. previousCell.model.set('weight', pcw);
  502. cellView.row.resize();
  503. }
  504. ui.helper.css('left', -handle.outerWidth()/2);
  505. }
  506. });
  507. },
  508. /**
  509. * This is triggered when ever a widget is added to the row collection.
  510. *
  511. * @param widget
  512. */
  513. onAddWidget: function(widget, collection, options){
  514. options = _.extend({noAnimate : false}, options);
  515. // Create the view for the widget
  516. var view = new panels.view.widget( {
  517. model: widget
  518. } );
  519. view.cell = this;
  520. if( typeof widget.isDuplicate === 'undefined' ) {
  521. widget.isDuplicate = false;
  522. }
  523. // Render and load the form if this is a duplicate
  524. view.render({
  525. 'loadForm': widget.isDuplicate
  526. });
  527. if( typeof options.at === 'undefined' || collection.length <= 1 ) {
  528. // Insert this at the end of the widgets container
  529. view.$el.appendTo( this.$( '.widgets-container' ) );
  530. }
  531. else {
  532. // We need to insert this at a specific position
  533. view.$el.insertAfter(
  534. this.$('.widgets-container .so-widget').eq( options.at - 1 )
  535. );
  536. }
  537. if( options.noAnimate === false ) {
  538. // We need an animation
  539. view.visualCreate();
  540. }
  541. this.refreshSortable();
  542. this.row.resize();
  543. },
  544. /**
  545. * Handle an action click on this cell
  546. *
  547. * @param e
  548. * @returns {boolean}
  549. */
  550. handleActionClick : function(e){
  551. return false;
  552. }
  553. } );
  554. /**
  555. * Model for a row of cells
  556. */
  557. panels.model.row = Backbone.Model.extend( {
  558. /* A collection of the cells in this row */
  559. cells: {},
  560. /* The builder model */
  561. builder: null,
  562. defaults :{
  563. style: {}
  564. },
  565. /**
  566. * Initialize the row model
  567. */
  568. initialize: function(){
  569. this.cells = new panels.collection.cells();
  570. this.on('destroy', this.onDestroy, this);
  571. },
  572. /**
  573. * Add cells to the model row
  574. *
  575. * @param cells an array of cells, where each object in the array has a weight value
  576. */
  577. setCells: function(cells){
  578. var thisModel = this;
  579. if( this.cells.length === 0 ) {
  580. // We're adding the initial cells
  581. _.each(cells, function (cellWeight) {
  582. // Add the new cell to the row
  583. var cell = new panels.model.cell({
  584. weight: cellWeight,
  585. collection: thisModel.cells
  586. });
  587. cell.row = thisModel;
  588. thisModel.cells.add(cell);
  589. });
  590. }
  591. else {
  592. if(cells.length > this.cells.length) {
  593. // We need to add cells
  594. for( var i = this.cells.length; i < cells.length; i++ ) {
  595. var cell = new panels.model.cell({
  596. weight: cells[ cells.length + i ],
  597. collection: thisModel.cells
  598. });
  599. cell.row = this;
  600. thisModel.cells.add(cell);
  601. }
  602. }
  603. else if(cells.length < this.cells.length) {
  604. // We need to remove cells
  605. _.each(this.cells.slice( cells.length, this.cells.length), function(cell){
  606. cell.destroy();
  607. });
  608. }
  609. // Now we need to change the weights of all the cells
  610. this.cells.each(function(cell, i){
  611. cell.set('weight', cells[i]);
  612. });
  613. }
  614. // Rescale the cells when we add or remove
  615. this.reweightCells();
  616. },
  617. /**
  618. * Make sure that all the cell weights add up to 1
  619. */
  620. reweightCells: function() {
  621. var totalWeight = 0;
  622. this.cells.each( function(cell){
  623. totalWeight += cell.get('weight');
  624. } );
  625. this.cells.each( function(cell){
  626. cell.set( 'weight', cell.get('weight') / totalWeight );
  627. } );
  628. // This is for the row view to hook into and resize
  629. this.trigger('reweight_cells');
  630. },
  631. /**
  632. * Triggered when the model is destroyed
  633. */
  634. onDestroy: function(){
  635. // Also destroy all the cells
  636. _.invoke(this.cells.toArray(), 'destroy');
  637. this.cells.reset();
  638. },
  639. /**
  640. * Create a clone of the row, along with all its cells
  641. *
  642. * @param {panels.model.builder} builder The builder model to attach this to.
  643. *
  644. * @return {panels.model.row} The cloned row.
  645. */
  646. clone: function( builder, cloneOptions ){
  647. if(typeof builder === 'undefined') {
  648. builder = this.builder;
  649. }
  650. cloneOptions = _.extend({ cloneCells: true }, cloneOptions);
  651. var clone = new this.constructor( this.attributes );
  652. clone.set('collection', builder.rows, {silent: true});
  653. clone.builder = builder;
  654. if( cloneOptions.cloneCells ) {
  655. // Clone all the rows
  656. this.cells.each(function(cell){
  657. clone.cells.add( cell.clone( clone, cloneOptions ), {silent: true});
  658. });
  659. }
  660. return clone;
  661. }
  662. } );
  663. /**
  664. * A collection of rows. This is used to represent the entire content of Page Builder.
  665. */
  666. panels.collection.rows = Backbone.Collection.extend( {
  667. model: panels.model.row,
  668. /**
  669. * Destroy all the rows in this collection
  670. */
  671. empty: function(){
  672. var model;
  673. do {
  674. model = this.collection.first();
  675. if( !model ) { break; }
  676. model.destroy();
  677. } while( true );
  678. }
  679. } );
  680. /**
  681. * View for handling the row.
  682. */
  683. panels.view.row = Backbone.View.extend( {
  684. template: _.template( $('#siteorigin-panels-builder-row').html().panelsProcessTemplate() ),
  685. events: {
  686. 'click .so-row-settings' : 'editSettingsHandler',
  687. 'click .so-row-duplicate' : 'duplicateHandler',
  688. 'click .so-row-delete' : 'confirmedDeleteHandler'
  689. },
  690. builder: null,
  691. dialog: null,
  692. /**
  693. * Initialize the row view
  694. */
  695. initialize: function(){
  696. this.model.cells.on('add', this.handleCellAdd, this);
  697. this.model.cells.on('remove', this.handleCellRemove, this);
  698. this.model.on('reweight_cells', this.resize, this);
  699. this.model.on('destroy', this.onModelDestroy, this);
  700. this.model.on('visual_destroy', this.visualDestroyModel, this);
  701. var thisView = this;
  702. this.model.cells.each(function(cell){
  703. thisView.listenTo(cell.widgets, 'add', thisView.resize);
  704. });
  705. // When ever a new cell is added, listen to it for new widgets
  706. this.model.cells.on('add', function(cell){
  707. thisView.listenTo(cell.widgets, 'add', thisView.resize);
  708. }, this);
  709. },
  710. /**
  711. * Render the row.
  712. *
  713. * @returns {panels.view.row}
  714. */
  715. render: function(){
  716. this.setElement( this.template() );
  717. this.$el.data('view', this);
  718. // Create views for the cells in this row
  719. var thisView = this;
  720. this.model.cells.each( function(cell){
  721. var cellView = new panels.view.cell({
  722. model: cell
  723. });
  724. cellView.row = thisView;
  725. cellView.render();
  726. cellView.$el.appendTo( thisView.$('.so-cells') );
  727. } );
  728. // Resize the rows when ever the widget sortable moves
  729. this.builder.on('widget_sortable_move', this.resize, this);
  730. this.builder.on('builder_resize', this.resize, this);
  731. this.resize();
  732. return this;
  733. },
  734. /**
  735. * Give a visual indication of the creation of this row
  736. */
  737. visualCreate: function(){
  738. this.$el.hide().fadeIn('fast');
  739. },
  740. /**
  741. * Visually resize the row so that all cell heights are the same and the widths so that they balance to 100%
  742. *
  743. * @param e
  744. */
  745. resize: function(e){
  746. // Don't resize this
  747. if( !this.$el.is(':visible') ) {
  748. return false;
  749. }
  750. // Reset everything to have an automatic height
  751. this.$el.find( '.so-cells .cell-wrapper' ).css( 'min-height', 0 );
  752. // We'll tie the values to the row view, to prevent issue with values going to different rows
  753. var height = 0;
  754. this.$el.find('.so-cells .cell').each( function () {
  755. height = Math.max(
  756. height,
  757. $(this ).height()
  758. );
  759. $( this ).css( 'width', ( $(this).data('view').model.get('weight') * 100 ) + "%" );
  760. } );
  761. // Resize all the grids and cell wrappers
  762. this.$el.find( '.so-cells .cell-wrapper' ).css( 'min-height', Math.max( height, 70 ) );
  763. },
  764. /**
  765. * Remove the view from the dom.
  766. */
  767. onModelDestroy: function() {
  768. this.remove();
  769. },
  770. /**
  771. * Fade out the view and destroy the model
  772. */
  773. visualDestroyModel: function(){
  774. this.builder.addHistoryEntry('row_deleted');
  775. var thisView = this;
  776. this.$el.fadeOut('normal', function(){
  777. thisView.model.destroy();
  778. thisView.builder.model.refreshPanelsData();
  779. if(thisView.builder.liveEditor.displayed) {
  780. thisView.builder.liveEditor.refreshWidgets();
  781. }
  782. });
  783. },
  784. /**
  785. * Duplicate this row.
  786. *
  787. * @return {boolean}
  788. */
  789. duplicateHandler: function(){
  790. this.builder.addHistoryEntry('row_duplicated');
  791. var duplicateRow = this.model.clone( this.builder.model );
  792. this.builder.model.rows.add( duplicateRow, {
  793. at: this.builder.model.rows.indexOf( this.model ) + 1
  794. } );
  795. return false;
  796. },
  797. /**
  798. * Handles deleting the row with a confirmation.
  799. */
  800. confirmedDeleteHandler: function(e){
  801. var $$ = $(e.target);
  802. // The user clicked on the dashicon
  803. if( $$.hasClass('dashicons') ) {
  804. $$ = $$.parent();
  805. }
  806. if( $$.hasClass('so-confirmed') ) {
  807. this.visualDestroyModel();
  808. }
  809. else {
  810. var originalText = $$.html();
  811. $$.addClass('so-confirmed').html(
  812. '<span class="dashicons dashicons-yes"></span>' + panelsOptions.loc.dropdown_confirm
  813. );
  814. setTimeout(function(){
  815. $$.removeClass('so-confirmed').html(originalText);
  816. }, 2500);
  817. }
  818. return false;
  819. },
  820. /**
  821. * Handle displaying the settings dialog
  822. */
  823. editSettingsHandler: function(){
  824. // Lets open up an instance of the settings dialog
  825. var dialog = this.builder.dialogs.row;
  826. if( this.dialog == null ) {
  827. // Create the dialog
  828. this.dialog = new panels.dialog.row();
  829. this.dialog.setBuilder( this.builder).setRowModel( this.model);
  830. }
  831. this.dialog.openDialog();
  832. return false;
  833. },
  834. /**
  835. * Handle deleting this entire row.
  836. */
  837. deleteHandler: function(){
  838. this.model.destroy();
  839. return false;
  840. },
  841. /**
  842. * Handle a new cell being added to this row view. For now we'll assume the new cell is always last
  843. */
  844. handleCellAdd: function(cell){
  845. var cellView = new panels.view.cell({
  846. model: cell
  847. });
  848. cellView.row = this;
  849. cellView.render();
  850. cellView.$el.appendTo( this.$('.so-cells') );
  851. },
  852. /**
  853. * Handle a cell being removed from this row view
  854. */
  855. handleCellRemove: function(cell){
  856. // Find the view that ties in to the cell we're removing
  857. this.$el.find('.so-cells > .cell').each( function(){
  858. var view = $(this).data('view');
  859. if(typeof view === 'undefined') {
  860. return false;
  861. }
  862. if( view.model.cid === cell.cid ) {
  863. // Remove this view
  864. view.remove();
  865. }
  866. } );
  867. }
  868. } );
  869. /**
  870. * The builder model
  871. */
  872. panels.model.builder = Backbone.Model.extend( {
  873. rows: {},
  874. defaults : {
  875. 'data' : {
  876. 'widgets' : [],
  877. 'grids' : [],
  878. 'grid_cells' : []
  879. }
  880. },
  881. initialize: function(){
  882. // These are the main rows in the interface
  883. this.rows = new panels.collection.rows();
  884. },
  885. /**
  886. * Add a new row to this builder.
  887. *
  888. * @param weights
  889. */
  890. addRow: function( weights, options ){
  891. options = _.extend({noAnimate : false}, options);
  892. // Create the actual row
  893. var row = new panels.model.row( {
  894. collection: this.rows
  895. } );
  896. row.setCells( weights );
  897. row.builder = this;
  898. this.rows.add(row, options);
  899. return row;
  900. },
  901. /**
  902. * Load the panels data into the builder
  903. *
  904. * @param data
  905. */
  906. loadPanelsData: function(data){
  907. // Start by destroying any rows that currently exist. This will in turn destroy cells, widgets and all the associated views
  908. this.emptyRows();
  909. // This will empty out the current rows and reload the builder data.
  910. this.set( 'data', data, {silent: true} );
  911. var cit = 0;
  912. var rows = [];
  913. if( typeof data.grid_cells === 'undefined' ) { return; }
  914. var gi;
  915. for(var ci = 0; ci < data.grid_cells.length; ci++) {
  916. gi = parseInt(data.grid_cells[ci].grid);
  917. if(typeof rows[gi] === 'undefined') {
  918. rows[gi] = [];
  919. }
  920. rows[gi].push( parseFloat( data.grid_cells[ci].weight ) );
  921. }
  922. var builderModel = this;
  923. _.each( rows, function(row, i){
  924. // This will create and add the row model and its cells
  925. var newRow = builderModel.addRow( row, { noAnimate: true } );
  926. if( typeof data.grids[i].style !== 'undefined' ) {
  927. newRow.set( 'style', data.grids[i].style );
  928. }
  929. } );
  930. if( typeof data.widgets === 'undefined' ) { return; }
  931. // Add the widgets
  932. _.each(data.widgets, function(widgetData){
  933. try {
  934. var panels_info = null;
  935. if (typeof widgetData.panels_info !== 'undefined') {
  936. panels_info = widgetData.panels_info;
  937. delete widgetData.panels_info;
  938. }
  939. else {
  940. panels_info = widgetData.info;
  941. delete widgetData.info;
  942. }
  943. var row = builderModel.rows.at( parseInt(panels_info.grid) );
  944. var cell = row.cells.at(parseInt(panels_info.cell));
  945. var newWidget = new panels.model.widget({
  946. class: panels_info.class,
  947. values: widgetData
  948. });
  949. if( typeof panels_info.style !== 'undefined' ) {
  950. newWidget.set('style', panels_info.style );
  951. }
  952. newWidget.cell = cell;
  953. cell.widgets.add(newWidget, {noAnimate: true});
  954. }
  955. catch (err) {
  956. }
  957. } );
  958. },
  959. /**
  960. * Convert the content of the builder into a object that represents the page builder data
  961. */
  962. getPanelsData: function(){
  963. var data = {
  964. 'widgets' : [],
  965. 'grids' : [],
  966. 'grid_cells' : []
  967. };
  968. var widgetId = 0;
  969. this.rows.each(function(row, ri){
  970. row.cells.each(function(cell, ci){
  971. cell.widgets.each(function(widget, wi){
  972. // Add the data for the widget, including the panels_info field.
  973. var values = _.extend( _.clone( widget.get('values') ), {
  974. panels_info : {
  975. class: widget.get('class'),
  976. raw: widget.get('raw'),
  977. grid: ri,
  978. cell: ci,
  979. id: widgetId++,
  980. style: widget.get('style')
  981. }
  982. } );
  983. data.widgets.push( values );
  984. });
  985. // Add the cell info
  986. data.grid_cells.push( {
  987. grid: ri,
  988. weight: cell.get('weight')
  989. } );
  990. });
  991. data.grids.push( {
  992. cells: row.cells.length,
  993. style: row.get('style')
  994. } );
  995. } );
  996. return data;
  997. },
  998. /**
  999. * This will check all the current entries and refresh the panels data
  1000. */
  1001. refreshPanelsData: function(){
  1002. var oldData = JSON.stringify( this.get('data') );
  1003. var newData = this.getPanelsData();
  1004. this.set( 'data', newData, { silent: true } );
  1005. if( JSON.stringify( newData ) !== oldData ) {
  1006. // The default change event doesn't trigger on deep changes, so we'll trigger our own
  1007. this.trigger('change');
  1008. this.trigger('change:data');
  1009. }
  1010. },
  1011. /**
  1012. * Empty all the rows and the cells/widgets they contain.
  1013. */
  1014. emptyRows: function(){
  1015. _.invoke(this.rows.toArray(), 'destroy');
  1016. this.rows.reset();
  1017. return this;
  1018. }
  1019. } );
  1020. /**
  1021. * This is the main view for the Page Builder interface.
  1022. */
  1023. panels.view.builder = Backbone.View.extend( {
  1024. template: _.template( $('#siteorigin-panels-builder').html().panelsProcessTemplate() ),
  1025. dialogs: { },
  1026. rowsSortable: null,
  1027. dataField : false,
  1028. currentData: '',
  1029. attachedToEditor: false,
  1030. liveEditor: false,
  1031. events: {
  1032. 'click .so-tool-button.so-widget-add': 'displayAddWidgetDialog',
  1033. 'click .so-tool-button.so-row-add': 'displayAddRowDialog',
  1034. 'click .so-tool-button.so-prebuilt-add': 'displayAddPrebuiltDialog',
  1035. 'click .so-tool-button.so-history': 'displayHistoryDialog',
  1036. 'click .so-tool-button.so-live-editor': 'displayLiveEditor',
  1037. 'click .so-cells .cell .cell-wrapper' : 'cellClickHandler'
  1038. },
  1039. /* A row collection */
  1040. rows: null,
  1041. /**
  1042. * Initialize the builder
  1043. */
  1044. initialize: function(){
  1045. var builder = this;
  1046. // Now lets create all the dialog boxes that the main builder interface uses
  1047. this.dialogs = {
  1048. widgets: new panels.dialog.widgets(),
  1049. row: new panels.dialog.row(),
  1050. prebuilt: new panels.dialog.prebuilt()
  1051. };
  1052. // Set the builder for each dialog and render it.
  1053. _.each(this.dialogs, function(p, i, d){
  1054. d[i].setBuilder( builder );
  1055. });
  1056. this.dialogs.row.setRowDialogType('create');
  1057. // This handles a new row being added to the collection - we'll display it in the interface
  1058. this.model.rows.on('add', this.onAddRow, this);
  1059. // Reflow the entire builder when ever the
  1060. $(window).resize(function(e){
  1061. if(e.target === window) {
  1062. builder.trigger('builder_resize');
  1063. }
  1064. });
  1065. // When the data changes in the model, store it in the field
  1066. this.model.on('change:data', this.storeModelData, this);
  1067. // Handle a content change
  1068. this.on('content_change', this.handleContentChange, this);
  1069. this.on('display_builder', this.handleDisplayBuilder, this);
  1070. this.model.on('change:data', this.toggleWelcomeDisplay, this);
  1071. },
  1072. /**
  1073. * Render the builder interface.
  1074. *
  1075. * @return {siteoriginPanels.view.builder}
  1076. */
  1077. render: function(){
  1078. this.$el.html( this.template() );
  1079. this.$el
  1080. .attr( 'id', 'siteorigin-panels-builder-' + this.cid )
  1081. .addClass('so-builder-container');
  1082. return this;
  1083. },
  1084. /**
  1085. * Attach the builder to the given container
  1086. *
  1087. * @param container
  1088. * @returns {panels.view.builder}
  1089. */
  1090. attach: function(options) {
  1091. options = _.extend({ container: false, dialog: false }, options);
  1092. if( options.dialog ) {
  1093. // We're going to add this to a dialog
  1094. this.dialog = new panels.dialog.builder();
  1095. this.dialog.builder = this;
  1096. }
  1097. else {
  1098. // Attach this in the standard way
  1099. this.$el.appendTo( options.container );
  1100. this.metabox = options.container.closest('.postbox');
  1101. this.initSortable();
  1102. }
  1103. return this;
  1104. },
  1105. /**
  1106. * This will move the Page Builder Metabox into the editor
  1107. *
  1108. * @returns {panels.view.builder}
  1109. */
  1110. attachToEditor: function(){
  1111. if( typeof this.metabox === 'undefined' ) {
  1112. return this;
  1113. }
  1114. this.attachedToEditor = true;
  1115. var metabox = this.metabox;
  1116. var thisView = this;
  1117. // Handle switching between the page builder and other tabs
  1118. $( '#wp-content-wrap .wp-editor-tabs' )
  1119. .find( '.wp-switch-editor' )
  1120. .click(function (e) {
  1121. e.preventDefault();
  1122. $( '#wp-content-editor-container, #post-status-info' ).show();
  1123. metabox.hide();
  1124. $( '#wp-content-wrap' ).removeClass('panels-active');
  1125. $('#content-resize-handle' ).show();
  1126. thisView.trigger('hide_builder');
  1127. } ).end()
  1128. .prepend(
  1129. $( '<a id="content-panels" class="hide-if-no-js wp-switch-editor switch-panels">' + metabox.find( 'h3.hndle span' ).html() + '</a>' )
  1130. .click( function (e) {
  1131. // Switch to the Page Builder interface
  1132. e.preventDefault();
  1133. var $$ = $( this );
  1134. // Hide the standard content editor
  1135. $( '#wp-content-wrap, #post-status-info' ).hide();
  1136. // Show page builder and the inside div
  1137. metabox.show().find('> .inside').show();
  1138. // Triggers full refresh
  1139. $( window ).resize();
  1140. $( document).scroll();
  1141. thisView.trigger('display_builder');
  1142. } )
  1143. );
  1144. // WordPress 4.1 changed the float of the tabs. Reorder them here.
  1145. // After WP 4.3 is released we'll make the new ordering default
  1146. if( $('body').hasClass('branch-4-1') || $('body').hasClass('branch-4-2') ) {
  1147. $( '#wp-content-wrap .wp-editor-tabs #content-panels' )
  1148. .appendTo( $( '#wp-content-wrap .wp-editor-tabs' ) );
  1149. }
  1150. // Switch back to the standard editor
  1151. metabox.find('.so-switch-to-standard').click(function(e){
  1152. e.preventDefault();
  1153. // Switch back to the standard editor
  1154. $( '#wp-content-wrap, #post-status-info' ).show();
  1155. metabox.hide();
  1156. // Resize to trigger reflow of WordPress editor stuff
  1157. $( window ).resize();
  1158. }).show();
  1159. // Move the panels box into a tab of the content editor
  1160. metabox.insertAfter( '#wp-content-wrap').hide().addClass('attached-to-editor');
  1161. // Switch to the Page Builder interface as soon as we load the page if there are widgets
  1162. var data = this.model.get('data');
  1163. if( typeof data.widgets !== 'undefined' && _.size(data.widgets) !== 0 ) {
  1164. $('#content-panels.switch-panels').click();
  1165. }
  1166. // We will also make this sticky if its attached to an editor.
  1167. var stickToolbar = function(){
  1168. var toolbar = thisView.$('.so-builder-toolbar');
  1169. var newTop = $(window).scrollTop() - thisView.$el.offset().top;
  1170. if( $('#wpadminbar').css('position') === 'fixed' ) {
  1171. newTop += $('#wpadminbar').outerHeight();
  1172. }
  1173. // Make sure this falls in an acceptible range.
  1174. newTop = Math.max( newTop, 0 );
  1175. newTop = Math.min( newTop, thisView.$el.outerHeight() - toolbar.outerHeight() + 20 ); // 20px extra to account for padding.
  1176. // Position the toolbar
  1177. toolbar.css('top', newTop);
  1178. thisView.$el.css('padding-top', toolbar.outerHeight());
  1179. };
  1180. $( window ).resize( stickToolbar );
  1181. $( document ).scroll( stickToolbar );
  1182. stickToolbar();
  1183. return this;
  1184. },
  1185. /**
  1186. * Initialize the row sortables
  1187. */
  1188. initSortable: function(){
  1189. // Create the sortable for the rows
  1190. var $el = this.$el;
  1191. var builderView = this;
  1192. this.rowsSortable = this.$el.find('.so-rows-container').sortable( {
  1193. appendTo: '#wpwrap',
  1194. items: '.so-row-container',
  1195. handle: '.so-row-move',
  1196. tolerance: 'pointer',
  1197. scroll: false,
  1198. stop: function (e) {
  1199. builderView.addHistoryEntry('row_moved');
  1200. // Sort the rows collection after updating all the indexes.
  1201. builderView.sortCollections();
  1202. }
  1203. } );
  1204. },
  1205. /**
  1206. * Refresh the row sortable
  1207. */
  1208. refreshSortable: function(){
  1209. // Refresh the sortable to account for the new row
  1210. if(this.rowsSortable !== null) {
  1211. this.rowsSortable.sortable('refresh');
  1212. }
  1213. },
  1214. /**
  1215. * Set the field that's used to store the data
  1216. * @param field
  1217. */
  1218. setDataField: function(field, options){
  1219. options = _.extend({
  1220. load: true
  1221. }, options);
  1222. this.dataField = field;
  1223. this.dataField.data('builder', this);
  1224. if( options.load && field.val() !== '') {
  1225. var data;
  1226. try {
  1227. data = JSON.parse( this.dataField.val( ) );
  1228. }
  1229. catch(err) {
  1230. data = '';
  1231. }
  1232. this.model.loadPanelsData(data);
  1233. this.currentData = data;
  1234. this.toggleWelcomeDisplay();
  1235. }
  1236. return this;
  1237. },
  1238. /**
  1239. * Store the model data in the data field set in this.setDataField.
  1240. */
  1241. storeModelData: function(){
  1242. var data = JSON.stringify( this.model.get('data' ) );
  1243. if( $(this.dataField).val() !== data ) {
  1244. // If the data is different, set it and trigger a content_change event
  1245. $(this.dataField).val( data );
  1246. this.trigger('content_change');
  1247. }
  1248. },
  1249. onAddRow: function(row, collection, options){
  1250. options = _.extend( {noAnimate: false}, options );
  1251. // Create a view for the row
  1252. var rowView = new panels.view.row( { model: row } );
  1253. rowView.builder = this;
  1254. rowView.render();
  1255. // Attach the row elements to this builder
  1256. if( typeof options.at === 'undefined' || collection.length <= 1 ) {
  1257. // Insert this at the end of the widgets container
  1258. rowView.$el.appendTo( this.$( '.so-rows-container' ) );
  1259. }
  1260. else {
  1261. // We need to insert this at a specific position
  1262. rowView.$el.insertAfter(
  1263. this.$('.so-rows-container .so-row-container'