PageRenderTime 79ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/so-widgets-bundle/base/js/posts-selector.js

https://gitlab.com/mostafame/team_website
JavaScript | 720 lines | 516 code | 126 blank | 78 comment | 65 complexity | dbd1f6331d9db7ab694ef538ee14c2e2 MD5 | raw file
  1. var soWidgetPostSelector = ( function ($, _) {
  2. var
  3. Post,
  4. PostCollection,
  5. PostCollectionSummaryView,
  6. PostCollectionView,
  7. PostSelectView,
  8. Query,
  9. QueryForm,
  10. QueryBuilder;
  11. // Model for a single post
  12. Post = self.Post = Backbone.Model.extend( {
  13. title : null,
  14. thumbnail : null,
  15. id : null
  16. } );
  17. // A collection of posts
  18. PostCollection = self.PostCollection = Backbone.Collection.extend( {
  19. model: Post,
  20. foundPosts: null,
  21. updateWithQuery: function(query){
  22. // Ignore empty queries
  23. if(query === '') {
  24. return;
  25. }
  26. // Reset the post collection by fetching the results from the server
  27. var c = this;
  28. $.post(
  29. sowPostsSelectorTpl.ajaxurl,
  30. { action: 'sow_get_posts', query: query, 'ignore_pagination' : true },
  31. function(data){
  32. c.foundPosts = data.found_posts;
  33. c.reset(data.posts);
  34. }
  35. );
  36. }
  37. } );
  38. // This represents a query that will be passed to the widget
  39. Query = self.Query = Backbone.Model.extend( {
  40. // The original query
  41. query: null,
  42. // The field we'll save and load from
  43. syncField: null,
  44. // get_posts fields
  45. post_type: null,
  46. terms: null,
  47. post_status: null,
  48. posts_per_page: null,
  49. post__in: null,
  50. tax_query: null,
  51. date_range: null,
  52. // The order fields for get_posts.
  53. orderby: null,
  54. order: null,
  55. sticky: null,
  56. defaults: {
  57. 'post_type' : 'post',
  58. 'orderby' : 'post_date',
  59. 'order' : 'DESC',
  60. 'posts_per_page' : '',
  61. 'post_status' : 'publish',
  62. 'sticky' : ''
  63. },
  64. initialize: function(params, options) {
  65. this.set( this.parseQuery(params.query) );
  66. },
  67. // Get the post query model as a WordPress get_posts query
  68. getQuery: function(){
  69. var query = [];
  70. if( typeof this.get('post_type') !== 'undefined' ) query.push('post_type=' + this.get('post_type'));
  71. if( typeof this.get('post__in') !== 'undefined' && !_.isEmpty( this.get('post__in') ) ) query.push( 'post__in=' + this.get('post__in').join(',') );
  72. if( typeof this.get('tax_query') !== 'undefined' && !_.isEmpty( this.get('tax_query') ) ) query.push( 'tax_query=' + this.get('tax_query').join(',') );
  73. if( typeof this.get('date_query') !== 'undefined' && !_.isEmpty( this.get('date_query') ) ) query.push( 'date_query=' + JSON.stringify(this.get('date_query')) );
  74. if( typeof this.get('orderby') !== 'undefined' ) query.push( 'orderby=' + this.get('orderby') );
  75. if( typeof this.get('order') !== 'undefined' ) query.push( 'order=' + this.get('order') );
  76. if( typeof this.get('posts_per_page') !== 'undefined' ) query.push( 'posts_per_page=' + this.get('posts_per_page') );
  77. if( typeof this.get('sticky') !== 'undefined' ) query.push( 'sticky=' + this.get('sticky') );
  78. if( typeof this.get('additional') !== 'undefined' ) query.push( 'additional=' + this.get('additional') );
  79. return query.join('&');
  80. },
  81. // Set the current query
  82. setQuery: function(query){
  83. this.set( this.parseQuery(query) );
  84. return this;
  85. },
  86. // Load a get_posts query string into this object.
  87. parseQuery: function( query ){
  88. var
  89. re = /([^&=]+)=?([^&]*)/g ,
  90. decodeRE = /\+/g ,
  91. decode = function (str) {return decodeURIComponent( str.replace(decodeRE, " ") );} ,
  92. params = {} ,
  93. e;
  94. while ( e = re.exec(query) ) {
  95. var k = decode( e[1] ), v = decode( e[2] );
  96. if (k.substring(k.length - 2) === '[]') {
  97. k = k.substring(0, k.length - 2);
  98. (params[k] || (params[k] = [])).push(v);
  99. }
  100. else params[k] = v;
  101. }
  102. // This is a simple array that we use to store parts of the query.
  103. var theQuery = {};
  104. if( params.hasOwnProperty('post_type') ) theQuery.post_type = params.post_type;
  105. if( params.hasOwnProperty('post__in') ) theQuery.post__in = params.post__in.split(',');
  106. if( params.hasOwnProperty('tax_query') ) theQuery.tax_query = params.tax_query.split(',');
  107. if( params.hasOwnProperty('date_query') ) theQuery.date_query = JSON.parse(params.date_query);
  108. if( params.hasOwnProperty('orderby') ) theQuery.orderby = params.orderby;
  109. if( params.hasOwnProperty('order') ) theQuery.order = params.order;
  110. if( params.hasOwnProperty('posts_per_page') ) theQuery.posts_per_page = params.posts_per_page;
  111. if( params.hasOwnProperty('sticky') ) theQuery.sticky = params.sticky;
  112. if( params.hasOwnProperty('additional') ) theQuery.additional = params.additional;
  113. theQuery.query = query;
  114. return theQuery;
  115. },
  116. sync: function( method, model ){
  117. if(method === 'create') {
  118. var curVal = this.syncField.val();
  119. var newVal = this.getQuery();
  120. if(curVal !== newVal) {
  121. this.syncField.val(newVal);
  122. this.syncField.trigger('change');
  123. }
  124. }
  125. else {
  126. this.setQuery( this.syncField.val() );
  127. }
  128. },
  129. // This is the field we'll sync the query to when it changes
  130. setSyncField: function( field ){
  131. this.syncField = field;
  132. }
  133. });
  134. // The main builder view. This handles all sub views
  135. QueryBuilder = self.QueryBuilder = Backbone.View.extend( {
  136. attached: false,
  137. rendered: false,
  138. views: {},
  139. activeView: null,
  140. events: {
  141. 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
  142. 'click .media-toolbar-primary .button': 'buttonHandler'
  143. },
  144. // Initialize the builder
  145. initialize: function() {
  146. // Listen for changes to the model
  147. this.listenTo(this.model, "change", this.queryModelChange);
  148. // Create the current posts summary view
  149. var postCollection = new PostCollection();
  150. this.views.postSummary = new PostCollectionSummaryView( {
  151. posts: postCollection,
  152. el: this.el
  153. } );
  154. this.views.postSummary.builder = this;
  155. this.views.postSummary.posts.updateWithQuery( this.model.getQuery() );
  156. // Create the sub views
  157. this.addSubView( 'form' , new QueryForm({ el: this.el, model: this.model }) );
  158. this.addSubView( 'postsView' , new PostCollectionView({ el: this.el, posts: postCollection }) );
  159. this.addSubView( 'postsSelect' , new PostSelectView({ el: this.el, model: this.model }) );
  160. // When the button is pressed in the form subview, close this
  161. this.views.form.bind('buttonHandler', this.close, this);
  162. },
  163. // Change the model we're using
  164. changeModel: function(model){
  165. this.model = model;
  166. this.render();
  167. },
  168. // Render the builder and the currently active sub view
  169. render: function() {
  170. // Create the modal
  171. this.$el.html( sowPostsSelectorTpl.modal );
  172. // Add the button from the sub view
  173. this.$el.find('.media-toolbar-primary .button').html( this.views[this.activeView].buttonText );
  174. this.$el.find('.media-frame-title h1').html( this.views[this.activeView].modalTitle );
  175. this.rendered = true;
  176. // Render the supporting views
  177. if(this.activeView !== 'postsSelect') {
  178. this.views.postSummary.render();
  179. }
  180. // Render the active view
  181. this.views[this.activeView].render();
  182. return this;
  183. },
  184. // Close and save the builder
  185. close: function(){
  186. this.$el.hide();
  187. this.trigger('close');
  188. this.model.save();
  189. return this;
  190. },
  191. // Open the builder
  192. open: function(){
  193. this.show();
  194. this.setActiveView('form');
  195. this.trigger('open');
  196. this.model.fetch();
  197. },
  198. // Save the model
  199. save: function(){
  200. this.close();
  201. this.model.save();
  202. this.trigger('save');
  203. },
  204. // Attach this builder to the body
  205. attach: function() {
  206. if ( ! this.rendered ) {
  207. this.render();
  208. }
  209. if( !this.attached ) {
  210. this.$el.appendTo('body');
  211. this.attached = true;
  212. }
  213. return this;
  214. },
  215. // Show the builder
  216. show: function(){
  217. this.attach();
  218. if( !this.$el.is(':visible') ) {
  219. this.$el.show();
  220. }
  221. },
  222. // Escape this
  223. escapeHandler: function(event){
  224. event.preventDefault();
  225. this.close();
  226. },
  227. // Handles the main button click on the frame
  228. buttonHandler: function(event){
  229. event.preventDefault();
  230. // Let the current view handle this button push...
  231. this.views[this.activeView].buttonHandler().trigger('buttonHandler');
  232. },
  233. // Add a subview to this builder
  234. addSubView: function( name, view ) {
  235. this.views[name] = view;
  236. view.builder = this;
  237. if(this.activeView === null) {
  238. this.activeView = name;
  239. }
  240. },
  241. // Set the active view
  242. setActiveView: function(name) {
  243. this.activeView = name;
  244. this.render();
  245. return;
  246. },
  247. queryModelChange: function(){
  248. this.views['postSummary'].posts.updateWithQuery( this.model.getQuery() );
  249. }
  250. } );
  251. // Handles the form for creating the query
  252. QueryForm = self.QueryForm = Backbone.View.extend( {
  253. buttonText: 'Save Query',
  254. modalTitle: 'Build Posts Query',
  255. form: null,
  256. initialize: function(params) {
  257. },
  258. render: function(){
  259. var thisView = this;
  260. // Add the fields
  261. this.form = $('<div class="query-builder-form>"></div>');
  262. // The post type field
  263. this.form.append('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.post_type + '</div>');
  264. if( typeof this.model.get('post_type') !== 'undefined' ) this.form.find('select[name="post_type"]').val( this.model.get('post_type'));
  265. // The post__in field
  266. this.form.append('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.post__in + '</div>');
  267. if( typeof this.model.get('post__in') !== 'undefined' ) this.form.find('input[name="post__in"]').val( this.model.get('post__in').join(',') );
  268. // The taxonomy field
  269. this.form.append('<div class="query-builder-form-field ui-front">' + sowPostsSelectorTpl.fields.tax_query + '</div>');
  270. if( typeof this.model.get('tax_query') !== 'undefined' ) this.form.find('input[name="tax_query"]').val( this.model.get('tax_query'));
  271. // The date range fields
  272. this.form.append('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.date_query + '</div>');
  273. if( typeof this.model.get('date_query') !== 'undefined' ) {
  274. var dateQuery = this.model.get('date_query');
  275. if( dateQuery.hasOwnProperty('after')) this.form.find('input[name="after"]').val(dateQuery.after);
  276. if( dateQuery.hasOwnProperty('before')) this.form.find('input[name="before"]').val(dateQuery.before);
  277. }
  278. // The order field
  279. this.form.append($('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.orderby + '</div>'));
  280. if( typeof this.model.get('orderby') !== 'undefined' ) this.form.find('select[name="orderby"]').val(this.model.get('orderby'));
  281. if( typeof this.model.get('order') !== 'undefined' ) this.form.find('input[name="order"]').val(this.model.get('order'));
  282. // The posts per page field
  283. this.form.append('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.posts_per_page + '</div>');
  284. if( typeof this.model.get('posts_per_page') !== 'undefined' ) this.form.find('input[name="posts_per_page"]').val( this.model.get('posts_per_page'));
  285. // The sticky posts field
  286. this.form.append('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.sticky + '</div>');
  287. if( typeof this.model.get('sticky') !== 'undefined' ) this.form.find('select[name="sticky"]').val( this.model.get('sticky'));
  288. // The additional query arguments field
  289. this.form.append('<div class="query-builder-form-field">' + sowPostsSelectorTpl.fields.additional + '</div>');
  290. if( typeof this.model.get('additional') !== 'undefined' ) this.form.find('input[name="additional"]').val( decodeURIComponent( this.model.get('additional') ) );
  291. var orderField = this.form.find('input[name="order"]');
  292. var orderButton = orderField.closest('.query-builder-form-field').find('.sow-order-button');
  293. // Reset the ordering button
  294. var resetOrderButton = function () {
  295. if (orderField.val() === 'DESC') {
  296. orderButton.removeClass('sow-order-button-asc');
  297. orderButton.addClass('sow-order-button-desc');
  298. }
  299. else {
  300. orderButton.addClass('sow-order-button-asc');
  301. orderButton.removeClass('sow-order-button-desc');
  302. }
  303. };
  304. resetOrderButton();
  305. orderButton.click(function(e){
  306. e.preventDefault();
  307. if(orderField.val() === 'DESC') {
  308. orderField.val('ASC');
  309. }
  310. else {
  311. orderField.val('DESC');
  312. }
  313. resetOrderButton();
  314. thisView.updateModel();
  315. return false;
  316. });
  317. // Add the form to the builder
  318. this.$el.find('.query-builder-content').empty().append(this.form);
  319. // Update the model when anything changes
  320. this.$el.find('.query-builder-form-field select, .query-builder-form-field input').change(function(){
  321. thisView.updateModel();
  322. });
  323. // When we click the select posts button, change to the posts select view
  324. this.$el.find('.query-builder-form-field .sow-select-posts').click(function(e){
  325. e.preventDefault();
  326. thisView.builder.setActiveView('postsSelect');
  327. });
  328. // Set up the autocomplete on the taxonomy query
  329. this.form.find('input[name="tax_query"]').autocomplete({
  330. source: function (request, response) {
  331. $.getJSON(
  332. sowPostsSelectorTpl.ajaxurl,
  333. {
  334. term: request.term.split(/,\s*/).pop(),
  335. action: 'sow_search_terms'
  336. },
  337. response
  338. );
  339. },
  340. search: function () {
  341. // custom minLength
  342. var term = this.value.split(/,\s*/).pop();
  343. if (term.length < 1) {
  344. return false;
  345. }
  346. },
  347. focus: function () {
  348. // prevent value inserted on focus
  349. return false;
  350. },
  351. select: function (event, ui) {
  352. var terms = this.value.split(/,\s*/);
  353. // remove the current input
  354. terms.pop();
  355. // add the selected item
  356. terms.push(ui.item.value);
  357. // add placeholder to get the comma-and-space at the end
  358. terms.push("");
  359. this.value = terms.join(", ");
  360. // Update the model after we've addded a new term
  361. thisView.updateModel();
  362. return false;
  363. }
  364. });
  365. return this;
  366. },
  367. // Update the model with what's in the form
  368. updateModel: function(){
  369. this.model.set( 'post_type', this.$el.find('*[name="post_type"]').val() );
  370. // Add the posts in part to the mode
  371. if(this.$el.find('*[name="post__in"]').val().trim() !== '') {
  372. this.model.set( 'post__in', this.$el.find('*[name="post__in"]').val().split(',').map(function(a){ return Number( a.trim() ); }) );
  373. }
  374. else {
  375. this.model.set( 'post__in', []);
  376. }
  377. // Build the taxonomy query
  378. if(this.$el.find('*[name="tax_query"]').val().trim() !== '') {
  379. var tax_query = this.$el.find('*[name="tax_query"]').val().split(',').map(function(a){ return a.trim(); });
  380. this.model.set( 'tax_query', _.compact(tax_query) );
  381. }
  382. else {
  383. this.model.set( 'tax_query', []);
  384. }
  385. this.model.set( 'date_query', {after: this.$el.find('*[name="after"]').val(), before: this.$el.find('*[name="before"]').val()});
  386. this.model.set( 'orderby', this.$el.find('*[name="orderby"]').val() );
  387. this.model.set( 'order', this.$el.find('*[name="order"]').val() );
  388. this.model.set( 'posts_per_page', this.$el.find('*[name="posts_per_page"]').val() );
  389. this.model.set( 'sticky', this.$el.find('*[name="sticky"]').val() );
  390. this.model.set( 'additional', encodeURIComponent( this.$el.find('*[name="additional"]').val() ) );
  391. this.model.set( 'query', this.model.getQuery() );
  392. return this;
  393. },
  394. // The button handler is triggered by QueryBuilder
  395. buttonHandler: function(){
  396. this.updateModel();
  397. return this;
  398. }
  399. } );
  400. // Displays a small count of the current number of queries
  401. PostCollectionSummaryView = self.PostCollectionSummaryView = Backbone.View.extend( {
  402. template: _.template(sowPostsSelectorTpl.foundPosts),
  403. posts: null,
  404. initialize: function(args) {
  405. // When ever the posts changes, we'll render the summary view
  406. this.posts = args.posts;
  407. this.posts.bind('reset', this.render, this);
  408. },
  409. render: function(){
  410. this.$el.find('.media-toolbar-secondary').html(this.template( {foundPosts : this.posts.foundPosts} ));
  411. var v = this;
  412. this.$el.find('.media-toolbar-secondary .preview-query-posts').click(function(e){
  413. e.preventDefault();
  414. v.builder.setActiveView('postsView');
  415. });
  416. }
  417. } );
  418. // Displays all the posts in the current collection
  419. PostCollectionView = self.PostCollectionView = Backbone.View.extend( {
  420. buttonText: 'Back',
  421. modalTitle: 'Current Posts',
  422. template: _.template(sowPostsSelectorTpl.postSummary),
  423. posts: null,
  424. initialize: function(args){
  425. this.posts = args.posts;
  426. },
  427. render: function(){
  428. var $c = this.$el.find('.query-builder-content').empty().append('<div class="sow-current-posts"></div>').find('.sow-current-posts');
  429. // Render all the posts
  430. $c = this.$el.find('.query-builder-content');
  431. var template = this.template;
  432. this.posts.each(function(post){
  433. $c.append(template(post.attributes));
  434. });
  435. return this;
  436. },
  437. // The button handler is triggered by QueryBuilder
  438. buttonHandler: function(){
  439. this.builder.setActiveView('form');
  440. return this;
  441. }
  442. } );
  443. // Select posts
  444. PostSelectView = self.PostSelectView = Backbone.View.extend( {
  445. buttonText: 'Finish Selection',
  446. modalTitle: 'Select Posts',
  447. sortable: null,
  448. postCache: {},
  449. postTemplate: _.template(sowPostsSelectorTpl.postSummary),
  450. initialize: function(){
  451. this.postCache = {};
  452. },
  453. render: function(){
  454. var posts = this.model.get('post__in');
  455. var postType = this.model.get('post_type');
  456. this.$el.find('.query-builder-content').empty().html(sowPostsSelectorTpl.selector);
  457. // Set up the sortable
  458. this.sortable = this.$el.find('.query-builder-content #sow-post-selector .sow-posts-sortable').sortable({
  459. placeholder: "ui-state-highlight",
  460. forcePlaceholderSize: true,
  461. items : '> .sow-post-selector-summary'
  462. });
  463. // Add any posts
  464. this.addPosts( posts );
  465. // Set up the autocomplete
  466. var v = this;
  467. var $searchInput = this.$el.find('.query-builder-content #sow-post-selector .sow-search-field');
  468. $searchInput.autocomplete({
  469. source: function(request, response) {
  470. request.type = postType;
  471. request.action = 'sow_search_posts';
  472. $.get(
  473. sowPostsSelectorTpl.ajaxurl,
  474. request,
  475. response
  476. );
  477. },
  478. minLength: 0,
  479. select: function( event, ui ) {
  480. event.preventDefault();
  481. $(this).val('');
  482. // Grab this new post and insert it
  483. v.addPosts([ui.item.value]);
  484. return false;
  485. }
  486. });
  487. $searchInput.focusin(
  488. function() {
  489. $searchInput.autocomplete("search", $searchInput.val());
  490. }
  491. )
  492. // Handle clicking on the remove buttons
  493. this.$el.find('.query-builder-content').on('click', '.sow-remove', function(e){
  494. e.preventDefault();
  495. var $$ = $(this);
  496. $$.closest('.sow-post-selector-summary').fadeOut('fast', function(){
  497. $(this).remove();
  498. v.sortable.sortable('refresh');
  499. });
  500. });
  501. return this;
  502. },
  503. addPosts: function(posts) {
  504. if(typeof posts === 'undefined' || _.isEmpty(posts)) {
  505. return;
  506. }
  507. var getPosts = [];
  508. for(var i = 0; i < posts.length; i++) {
  509. if(typeof this.postCache[ posts[i] ] === 'undefined') {
  510. getPosts.push(posts[i]);
  511. }
  512. }
  513. // Fetch any posts that we haven't already
  514. var v = this;
  515. if(!_.isEmpty(getPosts)) {
  516. $.post(
  517. sowPostsSelectorTpl.ajaxurl,
  518. {
  519. action: 'sow_get_posts',
  520. query : 'post_type=_all&posts_per_page=-1&post__in=' + getPosts.join(',')
  521. },
  522. function(data){
  523. if(typeof data.posts !== 'undefined') {
  524. _.each(data.posts, function(post, i){
  525. v.postCache[post.id] = {
  526. id : post.id,
  527. title : post.title,
  528. thumbnail : post.thumbnail,
  529. editUrl: post.editUrl
  530. };
  531. });
  532. }
  533. v.refreshLoading();
  534. }
  535. );
  536. }
  537. // Add placeholder posts
  538. var postItem;
  539. for(var i = 0; i < posts.length; i++) {
  540. if( typeof this.postCache[posts[i]] === 'undefined' ) {
  541. // Create a temporary post
  542. postItem = $(this.postTemplate( {id:posts[i], title: '', thumbnail: '', editUrl: '#'} )).addClass('sow-post-loading');
  543. }
  544. else {
  545. postItem = $(this.postTemplate( this.postCache[posts[i]] ));
  546. }
  547. postItem.appendTo(this.sortable);
  548. }
  549. this.sortable.sortable('refresh');
  550. return this;
  551. },
  552. refreshLoading: function(){
  553. var v = this;
  554. this.sortable.find('.sow-post-selector-summary.sow-post-loading').each(function(){
  555. var $$ = $(this);
  556. var id = $$.data('id');
  557. if(typeof v.postCache[id] !== 'undefined') {
  558. $$.removeClass('sow-post-loading');
  559. var postItem = $(v.postTemplate( v.postCache[ id ] ));
  560. $$.html( postItem.html() );
  561. }
  562. });
  563. },
  564. // The button handler is triggered by QueryBuilder
  565. buttonHandler: function(){
  566. // Update the post__in value
  567. var ids = [];
  568. this.sortable.find('.sow-post-selector-summary').each(function(){
  569. ids.push(Number($(this).data('id')));
  570. });
  571. this.model.set('post__in', ids);
  572. if(!_.isEmpty(ids)) {
  573. this.model.set('post_type', '_all');
  574. this.model.set('orderby', 'post__in');
  575. }
  576. this.builder.setActiveView('form');
  577. return this;
  578. }
  579. } );
  580. // The main QueryBuilder instance.
  581. var builder = new QueryBuilder( { model: new Query( { query: '' } ) } );
  582. jQuery( function($){
  583. $('body').on('click', '.sow-select-posts', function(e){
  584. e.preventDefault();
  585. var $postSelectorButton = $(this);
  586. builder.model.setSyncField( $postSelectorButton.siblings( '.siteorigin-widget-input' ) );
  587. builder.model.sync('update');
  588. builder.views.postSummary.posts.on("reset", function (postsCollection) {
  589. $postSelectorButton.find(".sow-current-count").text(postsCollection.foundPosts);
  590. });
  591. builder.open();
  592. });
  593. } );
  594. } )( jQuery, _ );