fl-builder-ui.js 36 KB

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