/* global tinymce, switchEditors */ /* eslint consistent-this: [ "error", "control" ] */ wp.textWidgets = ( function( $ ) { 'use strict'; var component = {}; /** * Text widget control. * * @class TextWidgetControl * @constructor * @abstract */ component.TextWidgetControl = Backbone.View.extend({ /** * View events. * * @type {Object} */ events: {}, /** * Initialize. * * @param {Object} options - Options. * @param {Backbone.Model} options.model - Model. * @param {jQuery} options.el - Control container element. * @returns {void} */ initialize: function initialize( options ) { var control = this; if ( ! options.el ) { throw new Error( 'Missing options.el' ); } Backbone.View.prototype.initialize.call( control, options ); /* * Create a container element for the widget control fields. * This is inserted into the DOM immediately before the the .widget-content * element because the contents of this element are essentially "managed" * by PHP, where each widget update cause the entire element to be emptied * and replaced with the rendered output of WP_Widget::form() which is * sent back in Ajax request made to save/update the widget instance. * To prevent a "flash of replaced DOM elements and re-initialized JS * components", the JS template is rendered outside of the normal form * container. */ control.fieldContainer = $( '
' ); control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) ); control.widgetContentContainer = control.$el.find( '.widget-content:first' ); control.widgetContentContainer.before( control.fieldContainer ); control.fields = { title: control.fieldContainer.find( '.title' ), text: control.fieldContainer.find( '.text' ) }; // Sync input fields to hidden sync fields which actually get sent to the server. _.each( control.fields, function( fieldInput, fieldName ) { fieldInput.on( 'input change', function updateSyncField() { var syncInput = control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ); if ( syncInput.val() !== $( this ).val() ) { syncInput.val( $( this ).val() ); syncInput.trigger( 'change' ); } }); // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event. fieldInput.val( control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ).val() ); }); }, /** * Update input fields from the sync fields. * * This function is called at the widget-updated and widget-synced events. * A field will only be updated if it is not currently focused, to avoid * overwriting content that the user is entering. * * @returns {void} */ updateFields: function updateFields() { var control = this, syncInput; if ( ! control.fields.title.is( document.activeElement ) ) { syncInput = control.widgetContentContainer.find( 'input[type=hidden].title' ); control.fields.title.val( syncInput.val() ); } syncInput = control.widgetContentContainer.find( 'input[type=hidden].text' ); if ( control.fields.text.is( ':visible' ) ) { if ( ! control.fields.text.is( document.activeElement ) ) { control.fields.text.val( syncInput.val() ); } } else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) { control.editor.setContent( wp.editor.autop( syncInput.val() ) ); } }, /** * Initialize editor. * * @returns {void} */ initializeEditor: function initializeEditor() { var control = this, changeDebounceDelay = 1000, id, textarea, restoreTextMode = false; textarea = control.fields.text; id = textarea.attr( 'id' ); /** * Build (or re-build) the visual editor. * * @returns {void} */ function buildEditor() { var editor, triggerChangeIfDirty, onInit; // Abort building if the textarea is gone, likely due to the widget having been deleted entirely. if ( ! document.getElementById( id ) ) { return; } // Destroy any existing editor so that it can be re-initialized after a widget-updated event. if ( tinymce.get( id ) ) { restoreTextMode = tinymce.get( id ).isHidden(); wp.editor.remove( id ); } wp.editor.initialize( id, { tinymce: { wpautop: true }, quicktags: true }); editor = window.tinymce.get( id ); if ( ! editor ) { throw new Error( 'Failed to initialize editor' ); } onInit = function() { // When a widget is moved in the DOM the dynamically-created TinyMCE iframe will be destroyed and has to be re-built. $( editor.getWin() ).on( 'unload', function() { _.defer( buildEditor ); }); // If a prior mce instance was replaced, and it was in text mode, toggle to text mode. if ( restoreTextMode ) { switchEditors.go( id, 'toggle' ); } }; if ( editor.initialized ) { onInit(); } else { editor.on( 'init', onInit ); } control.editorFocused = false; triggerChangeIfDirty = function() { var updateWidgetBuffer = 300; // See wp.customize.Widgets.WidgetControl._setupUpdateUI() which uses 250ms for updateWidgetDebounced. if ( editor.isDirty() ) { /* * Account for race condition in customizer where user clicks Save & Publish while * focus was just previously given to to the editor. Since updates to the editor * are debounced at 1 second and since widget input changes are only synced to * settings after 250ms, the customizer needs to be put into the processing * state during the time between the change event is triggered and updateWidget * logic starts. Note that the debounced update-widget request should be able * to be removed with the removal of the update-widget request entirely once * widgets are able to mutate their own instance props directly in JS without * having to make server round-trips to call the respective WP_Widget::update() * callbacks. See