fl-builder-ui.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224
  1. (function($, FLBuilder) {
  2. /**
  3. * Polyfill for String.startsWith()
  4. */
  5. if (!String.prototype.startsWith) {
  6. String.prototype.startsWith = function(searchString, position){
  7. position = position || 0;
  8. return this.substr(position, searchString.length) === searchString;
  9. };
  10. }
  11. /**
  12. * Polyfill for String.endsWidth()
  13. */
  14. if (!String.prototype.endsWith) {
  15. String.prototype.endsWith = function(searchString, position) {
  16. var subjectString = this.toString();
  17. if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
  18. position = subjectString.length;
  19. }
  20. position -= searchString.length;
  21. var lastIndex = subjectString.indexOf(searchString, position);
  22. return lastIndex !== -1 && lastIndex === position;
  23. };
  24. }
  25. // Calculate width of text from DOM element or string. By Phil Freo <http://philfreo.com>
  26. $.fn.textWidth = function(text, font) {
  27. if (!$.fn.textWidth.fakeEl) $.fn.textWidth.fakeEl = $('<span>').hide().appendTo(document.body);
  28. $.fn.textWidth.fakeEl.text(text || this.val() || this.text()).css('font', font || this.css('font'));
  29. return $.fn.textWidth.fakeEl.width();
  30. };
  31. /**
  32. * Base object that all view objects can delegate to.
  33. * Has the ability to create new objects with itself as the new object's prototype.
  34. */
  35. FLExtendableObject = {
  36. /**
  37. * Create new object with the current object set as its prototype.
  38. * @var mixin - Object with properties to be mixed into the final object.
  39. * @return object
  40. */
  41. create: function(mixin) {
  42. // create a new object with this object as it's prototype.
  43. var obj = Object.create(this);
  44. // mix any given properties into it
  45. obj = $.extend(obj, mixin);
  46. $(this).trigger('onCreate');
  47. return obj;
  48. },
  49. };
  50. /**
  51. * jQuery function to set a class while removing all other classes from
  52. * the same element that start with the prefix.
  53. * This is to allow for class states where only one state from the group of classes
  54. * can be present at any time.
  55. */
  56. $.fn.switchClass = function (prefix, ending) {
  57. return this.each(function() {
  58. $(this).removeClass(function(i, classesString) {
  59. var classesToRemove = [];
  60. var classes = classesString.split(' ');
  61. for(var i in classes) {
  62. if (classes[i].startsWith(prefix)) {
  63. classesToRemove.push(classes[i]);
  64. }
  65. }
  66. return classesToRemove.join(' ');
  67. });
  68. return $(this).addClass(prefix + ending);
  69. });
  70. };
  71. var KeyShortcuts = {
  72. /**
  73. * Initialize the keyboard shortcut manager.
  74. * @return void
  75. */
  76. init: function() {
  77. FLBuilder.addHook('cancelTask', this.onCancelTask.bind(this));
  78. FLBuilder.addHook('showSavedMessage', this.onSaveShortcut.bind(this));
  79. FLBuilder.addHook('goToNextTab', this.onNextPrevTabShortcut.bind(this, 'next'));
  80. FLBuilder.addHook('goToPrevTab', this.onNextPrevTabShortcut.bind(this, 'prev'));
  81. FLBuilder.addHook('endEditingSession', this.onEndEditingSession.bind(this));
  82. FLBuilder.addHook('restartEditingSession', this.onRestartEditingSession.bind(this));
  83. this.setDefaultKeyboardShortcuts();
  84. },
  85. /**
  86. * Add single keyboard shortcut
  87. * @var string A hook to be triggered by `FLBuilder.triggerhook(hook)`
  88. * @var string The key combination to trigger the command.
  89. * @var bool isGlobal - If the shortcut should work even inside inputs.
  90. * @return void
  91. */
  92. addShortcut: function( hook, key, isGlobal ) {
  93. var fn = $.proxy(this, 'onTriggerKey', hook);
  94. if ( isGlobal ) {
  95. Mousetrap.bindGlobal(key, fn);
  96. } else {
  97. Mousetrap.bind(key, fn);
  98. }
  99. },
  100. /**
  101. * Clear all registered key commands.
  102. * @return void
  103. */
  104. reset: function() {
  105. if ( ! FLBuilder.UIIFrame.isEnabled() ) {
  106. Mousetrap.reset();
  107. }
  108. },
  109. /**
  110. * Set the default shortcuts
  111. * @return void
  112. */
  113. setDefaultKeyboardShortcuts: function() {
  114. this.reset();
  115. for( var action in FLBuilderConfig.keyboardShortcuts ) {
  116. var code = FLBuilderConfig.keyboardShortcuts[action].keyCode,
  117. isGlobal = FLBuilderConfig.keyboardShortcuts[action].isGlobal;
  118. this.addShortcut( action, code, isGlobal);
  119. }
  120. },
  121. /**
  122. * Handle a key command by triggering the associated hook.
  123. * @var string the hook to be fired.
  124. * @return void
  125. */
  126. onTriggerKey: function(hook, e) {
  127. FLBuilder.triggerHook(hook);
  128. if (e.preventDefault) {
  129. e.preventDefault();
  130. } else {
  131. // internet explorer
  132. e.returnValue = false;
  133. }
  134. },
  135. /**
  136. * Cancel out of the current task - triggered by pressing ESC
  137. * @return void
  138. */
  139. onCancelTask: function() {
  140. // Is the editor in preview mode?
  141. if (EditingUI.isPreviewing) {
  142. EditingUI.endPreview();
  143. return;
  144. }
  145. // Are the publish actions showing?
  146. if (PublishActions.isShowing) {
  147. PublishActions.hide();
  148. return;
  149. }
  150. // Is the content panel showing?
  151. if (FLBuilder.ContentPanel.isShowing) {
  152. FLBuilder.ContentPanel.hide();
  153. return;
  154. }
  155. if ( FLBuilder.UIIFrame.isEnabled() ) {
  156. FLBuilder.UIIFrame.exitResponsiveEditing();
  157. return;
  158. }
  159. },
  160. /**
  161. * Pause the active keyboard shortcut listeners.
  162. * @return void
  163. */
  164. pause: function() {
  165. Mousetrap.pause();
  166. },
  167. /**
  168. * Unpause the active keyboard shortcut listeners.
  169. * @return void
  170. */
  171. unpause: function() {
  172. Mousetrap.unpause();
  173. },
  174. /**
  175. * Handle ending the editing session
  176. * @return void
  177. */
  178. onEndEditingSession: function() {
  179. const actions = FL.Builder.data.getSystemActions()
  180. actions.setIsEditing( false )
  181. document.documentElement.classList.remove( 'fl-builder-assistant-visible' )
  182. this.reset();
  183. this.addShortcut('restartEditingSession', 'mod+e');
  184. },
  185. /**
  186. * Handle restarting the editing session
  187. * @return void
  188. */
  189. onRestartEditingSession: function() {
  190. const actions = FL.Builder.data.getSystemActions()
  191. actions.setIsEditing( true )
  192. const currentPanel = FL.Builder.data.getSystemState().currentPanel
  193. if ( 'assistant' === currentPanel ) {
  194. document.documentElement.classList.add( 'fl-builder-assistant-visible' )
  195. }
  196. this.reset();
  197. this.setDefaultKeyboardShortcuts();
  198. },
  199. /**
  200. * Handle CMD+S Save Shortcut
  201. *
  202. * @return void
  203. */
  204. onSaveShortcut: function() {
  205. if (FLBuilder.SaveManager.layoutNeedsPublish()) {
  206. var message = FLBuilderStrings.savedStatus.hasAlreadySaved;
  207. FLBuilder.SaveManager.showStatusMessage(message);
  208. setTimeout(function() {
  209. FLBuilder.SaveManager.resetStatusMessage();
  210. }, 2000);
  211. } else {
  212. var message = FLBuilderStrings.savedStatus.nothingToSave;
  213. FLBuilder.SaveManager.showStatusMessage(message);
  214. setTimeout(function() {
  215. FLBuilder.SaveManager.resetStatusMessage();
  216. }, 2000);
  217. }
  218. },
  219. onNextPrevTabShortcut: function( direction, e ) {
  220. var $lightbox = $('.fl-lightbox:visible', window.parent.document),
  221. $tabs = $lightbox.find('.fl-builder-settings-tabs a'),
  222. $activeTab,
  223. $nextTab;
  224. if ( $lightbox.length > 0 ) {
  225. $activeTab = $tabs.filter('a.fl-active');
  226. if ( 'next' == direction ) {
  227. if ( $activeTab.is( $tabs.last() ) ) {
  228. $nextTab = $tabs.first();
  229. } else {
  230. $nextTab = $activeTab.next('a');
  231. }
  232. } else {
  233. if ( $activeTab.is( $tabs.first() ) ) {
  234. $nextTab = $tabs.last();
  235. } else {
  236. $nextTab = $activeTab.prev('a');
  237. }
  238. }
  239. $nextTab.trigger('click');
  240. }
  241. FLBuilder._calculateSettingsTabsOverflow();
  242. e.preventDefault();
  243. },
  244. };
  245. /**
  246. * Publish actions button bar UI
  247. */
  248. var PublishActions = FLExtendableObject.create({
  249. /**
  250. * Is the button bar showing?
  251. * @var bool
  252. */
  253. isShowing: false,
  254. /**
  255. * Setup the bar
  256. * @return void
  257. */
  258. init: function() {
  259. this.$el = $('.fl-builder-publish-actions', window.parent.document);
  260. this.$defaultBarButtons = $('.fl-builder-bar-actions', window.parent.document);
  261. this.$clickAwayMask = $('.fl-builder-publish-actions-click-away-mask', window.parent.document);
  262. this.$doneBtn = this.$defaultBarButtons.find('.fl-builder-done-button');
  263. this.$doneBtn.on('click', this.onDoneTriggered.bind(this));
  264. this.$actions = this.$el.find('.fl-builder-button');
  265. this.$actions.on('click touchend', this.onActionClicked.bind(this));
  266. FLBuilder.addHook('triggerDone', this.onDoneTriggered.bind(this));
  267. var hide = this.hide.bind(this);
  268. FLBuilder.addHook('cancelPublishActions', hide);
  269. FLBuilder.addHook('endEditingSession', hide);
  270. this.$clickAwayMask.on('click', hide );
  271. },
  272. /**
  273. * Fired when the done button is clicked or hook is triggered.
  274. * @return void
  275. */
  276. onDoneTriggered: function() {
  277. if (FLBuilder.SaveManager.layoutNeedsPublish()) {
  278. this.show();
  279. } else {
  280. if ( FLBuilderConfig.shouldRefreshOnPublish ) {
  281. FLBuilder._exit();
  282. } else {
  283. FLBuilder._exitWithoutRefresh();
  284. }
  285. }
  286. },
  287. /**
  288. * Display the publish actions.
  289. * @return void
  290. */
  291. show: function() {
  292. if (this.isShowing) return;
  293. // Save existing settings first if any exist. Don't proceed if it fails.
  294. if ( ! FLBuilder._triggerSettingsSave( false, true ) ) {
  295. return;
  296. }
  297. this.$el.removeClass('is-hidden');
  298. this.$defaultBarButtons.css('opacity', '0');
  299. this.$clickAwayMask.show();
  300. this.isShowing = true;
  301. FLBuilder.triggerHook('didShowPublishActions');
  302. },
  303. /**
  304. * Hide the publish actions.
  305. * @return void
  306. */
  307. hide: function() {
  308. if (!this.isShowing) return;
  309. this.$el.addClass('is-hidden');
  310. this.$defaultBarButtons.css('opacity', '1');
  311. this.$clickAwayMask.hide();
  312. this.isShowing = false;
  313. },
  314. /**
  315. * Fired when a publish action (or cancel) is clicked.
  316. * @return void
  317. */
  318. onActionClicked: function(e) {
  319. var action = $(e.currentTarget).data('action');
  320. switch(action) {
  321. case "dismiss":
  322. this.hide();
  323. break;
  324. case "discard":
  325. this.hide();
  326. EditingUI.muteToolbar();
  327. FLBuilder._discardButtonClicked();
  328. break;
  329. case "publish":
  330. this.hide();
  331. EditingUI.muteToolbar();
  332. FLBuilder._publishButtonClicked();
  333. FLBuilder._destroyOverlayEvents();
  334. break;
  335. case "draft":
  336. this.hide();
  337. EditingUI.muteToolbar();
  338. FLBuilder._draftButtonClicked();
  339. break;
  340. default:
  341. // draft
  342. this.hide();
  343. EditingUI.muteToolbar();
  344. FLBuilder._draftButtonClicked();
  345. }
  346. FLBuilder.triggerHook( action + 'ButtonClicked' );
  347. },
  348. });
  349. /**
  350. * Editing UI State Controller
  351. */
  352. var EditingUI = {
  353. /**
  354. * @var bool - whether or not the editor is in preview mode.
  355. */
  356. isPreviewing: false,
  357. /**
  358. * Setup the controller.
  359. * @return void
  360. */
  361. init: function() {
  362. this.$el = $('body', window.parent.document);
  363. this.$mainToolbar = $('.fl-builder-bar', window.parent.document);
  364. this.$mainToolbarContent = this.$mainToolbar.find('.fl-builder-bar-content', window.parent.document);
  365. this.$wpAdminBar = $('#wpadminbar');
  366. this.$endPreviewBtn = $('.fl-builder--preview-actions .end-preview-btn', window.parent.document);
  367. FLBuilder.addHook('endEditingSession', this.endEditingSession.bind(this) );
  368. FLBuilder.addHook('previewLayout', this.togglePreview.bind(this) );
  369. // End preview btn
  370. this.$endPreviewBtn.on('click', this.endPreview.bind(this));
  371. // Preview mode device size icons
  372. this.$deviceIcons = $('.fl-builder--preview-actions i', window.parent.document);
  373. this.$deviceIcons.on('click', this.onDeviceIconClick.bind(this));
  374. // Admin bar link to re-enable editor
  375. var $link = this.$wpAdminBar.find('#wp-admin-bar-fl-builder-frontend-edit-link > a, #wp-admin-bar-fl-theme-builder-frontend-edit-link > a');
  376. $link.on('click', this.onClickPageBuilderToolbarLink.bind(this));
  377. // Take admin bar links out of the tab order
  378. $('#wpadminbar a').attr('tabindex', '-1');
  379. var restart = this.restartEditingSession.bind(this);
  380. FLBuilder.addHook('restartEditingSession', restart);
  381. FLBuilder.addHook('didHideAllLightboxes', this.unmuteToolbar.bind(this));
  382. FLBuilder.addHook('didCancelDiscard', this.unmuteToolbar.bind(this));
  383. FLBuilder.addHook('didEnterRevisionPreview', this.hide.bind(this));
  384. FLBuilder.addHook('didExitRevisionPreview', this.show.bind(this));
  385. FLBuilder.addHook('didPublishLayout', this.onPublish.bind(this));
  386. // FLBuilder.addHook('didPublishLayout', this.onPublishCacheClear.bind(this));
  387. },
  388. /**
  389. * Handle exit w/o preview
  390. * @return void
  391. */
  392. endEditingSession: function() {
  393. FLBuilder._destroyOverlayEvents();
  394. FLBuilder._removeAllOverlays();
  395. FLBuilder._removeEmptyRowAndColHighlights();
  396. FLBuilder._unbindEvents();
  397. $('html').add( 'html', window.parent.document ).removeClass('fl-builder-edit').addClass('fl-builder-show-admin-bar');
  398. $('body').add( 'body', window.parent.document ).removeClass('fl-builder-edit');
  399. $('#wpadminbar a').attr('tabindex', null );
  400. $( FLBuilder._contentClass ).removeClass( 'fl-builder-content-editing' );
  401. this.hideMainToolbar();
  402. FLBuilder.ContentPanel.hide();
  403. FLBuilderLayout.init();
  404. },
  405. /**
  406. * Re-enter the editor without refresh after having left without refresh.
  407. * @return void
  408. */
  409. restartEditingSession: function(e) {
  410. FLBuilder._initTemplateSelector();
  411. FLBuilder._bindOverlayEvents();
  412. FLBuilder._highlightEmptyCols();
  413. FLBuilder._rebindEvents();
  414. $('html').add( 'html', window.parent.document ).addClass('fl-builder-edit').removeClass('fl-builder-show-admin-bar');
  415. $('body').add( 'html', window.parent.document ).addClass('fl-builder-edit');
  416. $('#wpadminbar a').attr('tabindex', '-1');
  417. $( FLBuilder._contentClass ).addClass( 'fl-builder-content-editing' );
  418. this.showMainToolbar();
  419. e.preventDefault();
  420. },
  421. /**
  422. * Handle re-entering the editor when you click the toolbar button.
  423. * @return void
  424. */
  425. onClickPageBuilderToolbarLink: function(e) {
  426. FLBuilder.triggerHook('restartEditingSession');
  427. e.preventDefault();
  428. },
  429. /**
  430. * Make admin bar dot green
  431. *
  432. * @return void
  433. */
  434. onPublish: function() {
  435. var $dot = this.$wpAdminBar.find('#wp-admin-bar-fl-builder-frontend-edit-link > a span');
  436. $dot.css('color', '#6bc373');
  437. },
  438. /**
  439. * Reload url via ajax, this rebuilds the cache files.
  440. */
  441. onPublishCacheClear: function() {
  442. FLBuilder.ajax({
  443. action: 'clear_cache_for_layout',
  444. }, function(response) {
  445. console.log(response);
  446. });
  447. },
  448. /**
  449. * Hides the entire UI.
  450. * @return void
  451. */
  452. hide: function() {
  453. if ( $( 'html' ).hasClass( 'fl-builder-edit' ) ) {
  454. FLBuilder._unbindEvents();
  455. FLBuilder._destroyOverlayEvents();
  456. FLBuilder._removeAllOverlays();
  457. $('html').add( 'html', window.parent.document ).removeClass('fl-builder-edit')
  458. $('body').removeClass('admin-bar');
  459. this.hideMainToolbar();
  460. FLBuilder.ContentPanel.hide();
  461. FLBuilderLayout.init();
  462. FLBuilder.triggerHook('didHideEditingUI');
  463. }
  464. },
  465. /**
  466. * Shows the UI when it's hidden.
  467. * @return void
  468. */
  469. show: function() {
  470. if ( ! $( 'html' ).hasClass( 'fl-builder-edit' ) ) {
  471. FLBuilder._rebindEvents();
  472. FLBuilder._bindOverlayEvents();
  473. this.showMainToolbar();
  474. FLBuilderResponsiveEditing._switchTo('default');
  475. $('html').add( 'html', window.parent.document ).addClass('fl-builder-edit');
  476. $('body').addClass('admin-bar');
  477. FLBuilder.triggerHook('didShowEditingUI');
  478. }
  479. },
  480. /**
  481. * Enter Preview Mode
  482. * @return void
  483. */
  484. beginPreview: function() {
  485. // Save existing settings first if any exist. Don't proceed if it fails.
  486. if ( ! FLBuilder._triggerSettingsSave( false, true ) ) {
  487. return;
  488. }
  489. this.isPreviewing = true;
  490. this.hide();
  491. $('html').add( 'html', window.parent.document ).addClass('fl-builder-preview');
  492. $('html, body').add( 'html, body', window.parent.document ).removeClass('fl-builder-edit');
  493. FLBuilder._removeEmptyRowAndColHighlights();
  494. FLBuilder.triggerHook('didBeginPreview');
  495. FLBuilderResponsivePreview.enter();
  496. },
  497. /**
  498. * Leave preview module
  499. * @return void
  500. */
  501. endPreview: function() {
  502. this.isPreviewing = false;
  503. this.show();
  504. FLBuilder._highlightEmptyCols();
  505. FLBuilderResponsivePreview.exit();
  506. $('html').add( 'html', window.parent.document ).removeClass('fl-builder-preview');
  507. $('html, body').add( 'html, body', window.parent.document ).addClass('fl-builder-edit');
  508. },
  509. /**
  510. * Toggle in and out of preview mode
  511. * @return void
  512. */
  513. togglePreview: function() {
  514. if (this.isPreviewing) {
  515. this.endPreview();
  516. } else {
  517. this.beginPreview();
  518. }
  519. },
  520. /**
  521. * Hide the editor toolbar
  522. * @return void
  523. */
  524. hideMainToolbar: function() {
  525. this.$mainToolbar.addClass('is-hidden');
  526. $( 'html', window.parent.document ).removeClass('fl-builder-is-showing-toolbar');
  527. },
  528. /**
  529. * Show the editor toolbar
  530. * @return void
  531. */
  532. showMainToolbar: function() {
  533. this.unmuteToolbar();
  534. this.$mainToolbar.removeClass('is-hidden');
  535. $( 'html', window.parent.document ).addClass('fl-builder-is-showing-toolbar');
  536. },
  537. /**
  538. * Handle clicking a responsive device icon while in preview
  539. * @return void
  540. */
  541. onDeviceIconClick: function(e) {
  542. var mode = $(e.target).data('mode');
  543. FLBuilderResponsivePreview.switchTo(mode);
  544. FLBuilderResponsivePreview._showSize(mode);
  545. },
  546. /**
  547. * Make toolbar innert
  548. * @return void
  549. */
  550. muteToolbar: function() {
  551. this.$mainToolbarContent.addClass('is-muted');
  552. FLBuilder._hideTipTips();
  553. },
  554. /**
  555. * Re-activate the toolbar
  556. * @return void
  557. */
  558. unmuteToolbar: function() {
  559. this.$mainToolbarContent.removeClass('is-muted');
  560. },
  561. };
  562. /**
  563. * Browser history logic.
  564. */
  565. var BrowserState = {
  566. isEditing: true,
  567. /**
  568. * Init the browser state controller
  569. *
  570. * @return void
  571. */
  572. init: function() {
  573. if ( history.pushState ) {
  574. FLBuilder.addHook('endEditingSession', this.onLeaveBuilder.bind(this) );
  575. FLBuilder.addHook('restartEditingSession', this.onEnterBuilder.bind(this) );
  576. }
  577. },
  578. /**
  579. * Handle restarting the edit session.
  580. *
  581. * @return void
  582. */
  583. onEnterBuilder: function() {
  584. history.replaceState( {}, document.title, FLBuilderConfig.editUrl );
  585. const actions = FL.Builder.data.getSystemActions()
  586. actions.setIsEditing( true )
  587. this.isEditing = true;
  588. },
  589. /**
  590. * Handle exiting the builder.
  591. *
  592. * @return void
  593. */
  594. onLeaveBuilder: function() {
  595. history.replaceState( {}, document.title, FLBuilderConfig.url );
  596. const actions = FL.Builder.data.getSystemActions()
  597. actions.setIsEditing( false )
  598. this.isEditing = false;
  599. },
  600. };
  601. /**
  602. * Content Library Search
  603. */
  604. var SearchUI = {
  605. /**
  606. * Setup the search controller
  607. * @return void
  608. */
  609. init: function() {
  610. this.$searchBox = $('.fl-builder--search', window.parent.document);
  611. this.$searchBoxInput = this.$searchBox.find('input#fl-builder-search-input');
  612. this.$searchBoxClear = this.$searchBox.find('.search-clear');
  613. this.$searchBoxInput.on('focus', this.onSearchInputFocus.bind(this));
  614. this.$searchBoxInput.on('blur', this.onSearchInputBlur.bind(this));
  615. this.$searchBoxInput.on('keyup', this.onSearchTermChange.bind(this));
  616. this.$searchBoxClear.on('click', this.onSearchTermClearClicked.bind(this));
  617. this.renderSearchResults = wp.template('fl-search-results-panel');
  618. this.renderNoResults = wp.template('fl-search-no-results');
  619. FLBuilder.addHook('didStartDrag', this.hideSearchResults.bind(this));
  620. FLBuilder.addHook('focusSearch', this.focusSearchBox.bind(this));
  621. },
  622. focusSearchBox: function() {
  623. this.$searchBoxInput.trigger('focus');
  624. },
  625. /**
  626. * Fires when focusing on the search field.
  627. * @return void
  628. */
  629. onSearchInputFocus: function() {
  630. this.$searchBox.addClass('is-expanded');
  631. FLBuilder.triggerHook('didFocusSearchBox');
  632. },
  633. /**
  634. * Fires when blurring out of the search field.
  635. * @return void
  636. */
  637. onSearchInputBlur: function(e) {
  638. this.$searchBox.removeClass('is-expanded has-text');
  639. this.$searchBoxInput.val('');
  640. this.hideSearchResults();
  641. },
  642. /**
  643. * Fires when a key is pressed inside the search field.
  644. * @return void
  645. */
  646. onSearchTermChange: function(e) {
  647. if (e.key == 'Escape') {
  648. this.$searchBoxInput.blur();
  649. return;
  650. }
  651. FLBuilder.triggerHook('didBeginSearch');
  652. var value = this.$searchBoxInput.val();
  653. if (value != '') {
  654. this.$searchBox.addClass('has-text');
  655. } else {
  656. this.$searchBox.removeClass('has-text');
  657. }
  658. var results = FLBuilder.Search.byTerm(value);
  659. if (results.term != "") {
  660. this.showSearchResults(results);
  661. } else {
  662. this.hideSearchResults();
  663. }
  664. },
  665. /**
  666. * Fires when the clear button is clicked.
  667. * @return void
  668. */
  669. onSearchTermClearClicked: function() {
  670. this.$searchBox.removeClass('has-text').addClass('is-expanded');
  671. this.$searchBoxInput.val('').focus();
  672. this.hideSearchResults();
  673. },
  674. /**
  675. * Display the found results in the results panel.
  676. * @var Object - the found results
  677. * @return void
  678. */
  679. showSearchResults: function(data) {
  680. if (data.total > 0) {
  681. var $html = $(this.renderSearchResults(data)),
  682. $panel = $('.fl-builder--search-results-panel', window.parent.document);
  683. $panel.html($html);
  684. FLBuilder._initSortables();
  685. } else {
  686. var $html = $(this.renderNoResults(data)),
  687. $panel = $('.fl-builder--search-results-panel', window.parent.document);
  688. $panel.html($html);
  689. }
  690. $('body', window.parent.document).addClass('fl-builder-search-results-panel-is-showing');
  691. },
  692. /**
  693. * Hide the search results panel
  694. * @return void
  695. */
  696. hideSearchResults: function() {
  697. $('body', window.parent.document).removeClass('fl-builder-search-results-panel-is-showing');
  698. },
  699. };
  700. var RowResize = {
  701. /**
  702. * @var {jQuery}
  703. */
  704. $row: null,
  705. /**
  706. * @var {jQuery}
  707. */
  708. $rowContent: null,
  709. /**
  710. * @var {Object}
  711. */
  712. row: null,
  713. /**
  714. * @var {Object}
  715. */
  716. drag: {},
  717. /**
  718. * Setup basic events for row content overlays
  719. * @return void
  720. */
  721. init: function() {
  722. if ( this.userCanResize() ) {
  723. var $layoutContent = $( FLBuilder._contentClass );
  724. $layoutContent.on( 'mouseenter touchstart', '.fl-row, .fl-block-overlay', this.onDragHandleHover.bind(this) );
  725. $layoutContent.on( 'mousedown touchstart', '.fl-block-row-resize', this.onDragHandleDown.bind(this) );
  726. }
  727. },
  728. /**
  729. * Check if the user is able to resize rows
  730. *
  731. * @return bool
  732. */
  733. userCanResize: function() {
  734. return FLBuilderConfig.rowResize.userCanResizeRows;
  735. },
  736. /**
  737. * Hover over a row resize drag handle.
  738. * @return void
  739. */
  740. onDragHandleHover: function(e) {
  741. if (this.drag.isDragging) {
  742. return
  743. };
  744. var $this = this,
  745. originalWidth,
  746. $handle = $(e.target),
  747. row = $handle.closest('.fl-row'),
  748. node = row.data('node'),
  749. form = $( '.fl-builder-row-settings[data-node=' + node + ']', window.parent.document ),
  750. unitField = form.find( '[name=max_content_width_unit]' ),
  751. unit = 'px';
  752. $this.onSettingsReady(node, function(settings){
  753. // Get unit.
  754. if (unitField.length) {
  755. unit = unitField.val();
  756. } else if ('undefined' !== typeof settings) {
  757. unit = settings.max_content_width_unit;
  758. }
  759. $this.$row = row;
  760. $this.$rowContent = $this.$row.find('.fl-row-content');
  761. $this.row = {
  762. node: node,
  763. form: form,
  764. unit: unit,
  765. isFixedWidth: $this.$row.hasClass('fl-row-fixed-width'),
  766. parentWidth: 'vw' === unit ? $( window ).width() : $this.$row.parent().width(),
  767. };
  768. $this.drag = {
  769. edge: null,
  770. isDragging: false,
  771. originalPosition: null,
  772. originalWidth: null,
  773. calculatedWidth: null,
  774. operation: null,
  775. };
  776. if ($this.row.isFixedWidth) {
  777. $this.drag.originalWidth = $this.$row.width();
  778. } else {
  779. $this.drag.originalWidth = $this.$rowContent.width();
  780. }
  781. $this.dragInit();
  782. });
  783. },
  784. /**
  785. * Check if FLBuilderSettingsConfig.node is available.
  786. * @return void
  787. */
  788. onSettingsReady: function(nodeId, callback) {
  789. var nodes = 'undefined' !== typeof FLBuilderSettingsConfig.nodes ? FLBuilderSettingsConfig.nodes : null;
  790. if (null !== nodes && 'undefined' !== typeof nodes[ nodeId ] ) {
  791. callback( nodes[ nodeId ] );
  792. if (null != RowResize._mouseEnterTimeout) {
  793. clearTimeout( RowResize._mouseEnterTimeout );
  794. RowResize._mouseEnterTimeout = null;
  795. }
  796. } else {
  797. // If settings is not yet available, check again by timeout.
  798. clearTimeout( RowResize._mouseEnterTimeout );
  799. RowResize._mouseEnterTimeout = setTimeout(this.onSettingsReady.bind(this), 350, nodeId, callback);
  800. }
  801. },
  802. /**
  803. * Handle mouse down on the drag handle
  804. * @return void
  805. */
  806. onDragHandleDown: function() {
  807. $('body').add( 'body', window.parent.document ).addClass( 'fl-builder-row-resizing' );
  808. if (null != RowResize._mouseEnterTimeout) {
  809. clearTimeout( RowResize._mouseEnterTimeout );
  810. RowResize._mouseEnterTimeout = null;
  811. }
  812. },
  813. /**
  814. * Setup the draggable handler
  815. * @return void
  816. */
  817. dragInit: function(e) {
  818. this.$row.find('.fl-block-row-resize').draggable( {
  819. axis : 'x',
  820. start : this.dragStart.bind(this),
  821. drag : this.dragging.bind(this),
  822. stop : this.dragStop.bind(this)
  823. });
  824. },
  825. /**
  826. * Handle drag started
  827. * @var {Event}
  828. * @var {Object}
  829. * @return void
  830. */
  831. dragStart: function(e, ui) {
  832. var body = $( 'body' ).add( 'body', window.parent.document ),
  833. $handle = $(ui.helper);
  834. this.drag.isDragging = true;
  835. if (this.row.isFixedWidth) {
  836. this.drag.originalWidth = this.$row.width();
  837. } else {
  838. this.drag.originalWidth = this.$rowContent.width();
  839. }
  840. if ($handle.hasClass( 'fl-block-col-resize-e' )) {
  841. this.drag.edge = 'e';
  842. this.$feedback = $handle.find('.fl-block-col-resize-feedback-left');
  843. }
  844. if ($handle.hasClass( 'fl-block-col-resize-w' )) {
  845. this.drag.edge = 'w';
  846. this.$feedback = $handle.find('.fl-block-col-resize-feedback-right');
  847. }
  848. body.addClass( 'fl-builder-row-resizing' );
  849. FLBuilder._colResizing = true;
  850. FLBuilder._destroyOverlayEvents();
  851. FLBuilder._closePanel();
  852. },
  853. /**
  854. * Handle drag
  855. * @var {Event}
  856. * @var {Object}
  857. * @return void
  858. */
  859. dragging: function(e, ui) {
  860. var currentPosition = ui.position.left,
  861. originalPosition = ui.originalPosition.left,
  862. originalWidth = this.drag.originalWidth,
  863. distance = 0,
  864. edge = this.drag.edge,
  865. minAllowedWidth = FLBuilderConfig.rowResize.minAllowedWidth,
  866. maxAllowedWidth = FLBuilderConfig.rowResize.maxAllowedWidth;
  867. if ( FLBuilderConfig.isRtl ) {
  868. edge = ( 'w' == edge ) ? 'e' : 'w'; // Flip the direction
  869. }
  870. if (originalPosition > currentPosition) {
  871. if (edge === 'w') {
  872. this.drag.operation = '+';
  873. } else {
  874. this.drag.operation = '-';
  875. }
  876. } else {
  877. if (edge === 'e') {
  878. this.drag.operation = '+';
  879. } else {
  880. this.drag.operation = '-';
  881. }
  882. }
  883. distance = Math.abs(originalPosition - currentPosition);
  884. if (this.drag.operation === '+') {
  885. this.drag.calculatedWidth = originalWidth + (distance * 2);
  886. } else {
  887. this.drag.calculatedWidth = originalWidth - (distance * 2);
  888. }
  889. if ( false !== minAllowedWidth && this.drag.calculatedWidth < minAllowedWidth ) {
  890. this.drag.calculatedWidth = minAllowedWidth;
  891. }
  892. if ( false !== maxAllowedWidth && this.drag.calculatedWidth > maxAllowedWidth ) {
  893. this.drag.calculatedWidth = maxAllowedWidth;
  894. }
  895. if (this.row.isFixedWidth) {
  896. this.$row.css('max-width', this.drag.calculatedWidth + 'px');
  897. }
  898. this.$rowContent.css('max-width', this.drag.calculatedWidth + 'px');
  899. if ( 'px' !== this.row.unit ) {
  900. this.drag.calculatedWidth = Math.round( this.drag.calculatedWidth / this.row.parentWidth * 100 );
  901. }
  902. if (!_.isUndefined(this.$feedback)) {
  903. this.$feedback.html(this.drag.calculatedWidth + this.row.unit).show();
  904. }
  905. if ( this.row.form.length ) {
  906. this.row.form.find( '[name=max_content_width]' ).val( this.drag.calculatedWidth );
  907. }
  908. // Dispatch update to store
  909. requestAnimationFrame( () => {
  910. const actions = FL.Builder.data.getLayoutActions()
  911. actions.resizeRowContent( this.row.node, this.drag.calculatedWidth, false )
  912. } )
  913. },
  914. /**
  915. * Handle drag ended
  916. * @var {Event}
  917. * @var {Object}
  918. * @return void
  919. */
  920. dragStop: function(e, ui) {
  921. this.drag.isDragging = false;
  922. if (!_.isUndefined(this.$feedback)) {
  923. this.$feedback.hide();
  924. }
  925. // Dispatch update to store
  926. const actions = FL.Builder.data.getLayoutActions()
  927. actions.resizeRowContent( this.row.node, this.drag.calculatedWidth )
  928. FLBuilder._bindOverlayEvents();
  929. $( 'body' ).add( 'body', window.parent.document ).removeClass( 'fl-builder-row-resizing' );
  930. requestAnimationFrame( () => {
  931. $( '.fl-block-overlay' ).each( function() {
  932. FLBuilder._buildOverlayOverflowMenu( $( this ) );
  933. } );
  934. } )
  935. // Set the resizing flag to false with a timeout so other events get the right value.
  936. setTimeout( function() { FLBuilder._colResizing = false; }, 50 );
  937. FLBuilder.triggerHook( 'didResizeRow', {
  938. rowId : this.row.node,
  939. rowWidth : this.drag.calculatedWidth
  940. } );
  941. },
  942. };
  943. var Toolbar = {
  944. /**
  945. * wp.template id suffix
  946. */
  947. templateName: 'fl-toolbar',
  948. /**
  949. * Initialize the toolbar controller
  950. *
  951. * @return void
  952. */
  953. init: function() {
  954. this.template = wp.template(this.templateName);
  955. this.render();
  956. this.initTipTips();
  957. /* "Add Content" Button */
  958. var $addContentBtn = this.$el.find('.fl-builder-content-panel-button');
  959. $addContentBtn.on('click', FLBuilder._togglePanel );
  960. this.$el.find('.fl-builder-buy-button').on('click', FLBuilder._upgradeClicked);
  961. this.$el.find('.fl-builder-upgrade-button').on('click', FLBuilder._upgradeClicked);
  962. this.$el.find('#fl-builder-toggle-notifications').on('click', this.onNotificationsButtonClicked.bind(this) );
  963. FLBuilder.addHook('notificationsLoaded', this.onNotificationsLoaded.bind(this));
  964. },
  965. /**
  966. * Render the toolbar html
  967. * @param object
  968. * @return void
  969. */
  970. render: function(data) {
  971. var $html = $(this.template(data));
  972. this.$el = $html;
  973. this.el = $html.get(0);
  974. EditingUI.$mainToolbar = this.$el;
  975. $( 'html', window.parent.document ).addClass( 'fl-builder-is-showing-toolbar' );
  976. $( 'body', window.parent.document ).prepend( $html );
  977. },
  978. /**
  979. * Add tooltips
  980. *
  981. * @return void
  982. */
  983. initTipTips: function() {
  984. // Publish actions tooltip
  985. $('.fl-builder-publish-actions .fl-builder-button-group .fl-builder-button', window.parent.document).tipTip({
  986. defaultPosition: 'bottom',
  987. edgeOffset: 6
  988. });
  989. },
  990. onNotificationsButtonClicked: function() {
  991. FLBuilder.triggerHook('toggleNotifications');
  992. },
  993. onNotificationsLoaded: function() {
  994. $('body').add( 'body', window.parent.document ).removeClass('fl-builder-has-new-notifications');
  995. var data = {
  996. action: 'fl_builder_notifications',
  997. read: true,
  998. }
  999. FLBuilder.ajax(data);
  1000. }
  1001. };
  1002. /**
  1003. * Kick off initializers when FLBuilder inits.
  1004. */
  1005. $(function() {
  1006. // Render Order matters here
  1007. FLBuilder.ContentPanel.init();
  1008. if ( !FLBuilderConfig.simpleUi ) {
  1009. FLBuilder.MainMenu.init();
  1010. }
  1011. if ( FLBuilderConfig.showToolbar ) {
  1012. Toolbar.init();
  1013. FLBuilder.ContentPanel.alignPanelArrow();
  1014. } else {
  1015. $('html').add( 'html', window.parent.document ).addClass('fl-builder-no-toolbar');
  1016. }
  1017. // End Render Order
  1018. KeyShortcuts.init();
  1019. EditingUI.init();
  1020. BrowserState.init();
  1021. RowResize.init();
  1022. PublishActions.init();
  1023. FLBuilder.triggerHook( 'didInitUI' );
  1024. });
  1025. })(jQuery, FLBuilder);