// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('goog.ui.equation.TexPane'); goog.require('goog.Timer'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.dom.selection'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.events.InputHandler'); goog.require('goog.style'); goog.require('goog.ui.equation.ChangeEvent'); goog.require('goog.ui.equation.EditorPane'); goog.require('goog.ui.equation.ImageRenderer'); goog.require('goog.ui.equation.Palette'); goog.require('goog.ui.equation.PaletteEvent'); /** * User interface for TeX equation editor tab pane. * @param {Object} context The context this Tex editor pane runs in. * @param {string} helpUrl The help link URL. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper. * @constructor * @extends {goog.ui.equation.EditorPane} * @final */ goog.ui.equation.TexPane = function( context, helpUrl, opt_domHelper) { goog.ui.equation.EditorPane.call(this, opt_domHelper); this.setHelpUrl(helpUrl); /** * The palette manager instance. * @type {goog.ui.equation.PaletteManager} * @private */ this.paletteManager_ = /** @type {goog.ui.equation.PaletteManager} */( context.paletteManager); }; goog.inherits(goog.ui.equation.TexPane, goog.ui.equation.EditorPane); /** * The CSS class name for the preview container. * @type {string} */ goog.ui.equation.TexPane.PREVIEW_CONTAINER_CSS_CLASS = 'ee-preview-container'; /** * The CSS class name for section titles. * @type {string} */ goog.ui.equation.TexPane.SECTION_TITLE_CSS_CLASS = 'ee-section-title'; /** * The CSS class name for section titles that float left. * @type {string} */ goog.ui.equation.TexPane.SECTION_TITLE_FLOAT_CSS_CLASS = 'ee-section-title-floating'; /** * The CSS id name for the link to "Learn more". * @type {string} */ goog.ui.equation.TexPane.SECTION_LEARN_MORE_CSS_ID = 'ee-section-learn-more'; /** * The CSS class name for the Tex editor. * @type {string} */ goog.ui.equation.TexPane.TEX_EDIT_CSS_CLASS = 'ee-tex'; /** * The CSS class name for the preview container. * @type {string} */ goog.ui.equation.TexPane.WARNING_CLASS = 'ee-warning'; /** * The content div of the TeX editor. * @type {Element} * @private */ goog.ui.equation.TexPane.prototype.texEditorElement_ = null; /** * The container div for the server-generated image of the equation. * @type {Element} * @private */ goog.ui.equation.TexPane.prototype.previewContainer_; /** * An inner container used to layout all the elements in Tex Editor. * @type {Element} * @private */ goog.ui.equation.TexPane.prototype.innerContainer_; /** * The textarea for free form TeX. * @type {Element} * @private */ goog.ui.equation.TexPane.prototype.texEdit_; /** * The input handler for Tex editor. * @type {goog.events.InputHandler} * @private */ goog.ui.equation.TexPane.prototype.texInputHandler_; /** * The last text that was renderred as an image. * @type {string} * @private */ goog.ui.equation.TexPane.prototype.lastRenderredText_ = ''; /** * A sequence number for text change events. Used to delay drawing * until the user paused typing. * @type {number} * @private */ goog.ui.equation.TexPane.prototype.changeSequence_ = 0; /** @override */ goog.ui.equation.TexPane.prototype.createDom = function() { /** @desc Title for TeX editor tab in the equation editor dialog. */ var MSG_EE_TEX_EQUATION = goog.getMsg('TeX Equation'); /** @desc Title for equation preview image in the equation editor dialog. */ var MSG_EE_TEX_PREVIEW = goog.getMsg('Preview'); /** @desc Link text that leads to an info page about the equation dialog. */ var MSG_EE_LEARN_MORE = goog.getMsg('Learn more'); var domHelper = this.dom_; var innerContainer; var texEditorEl = domHelper.createDom(goog.dom.TagName.DIV, {'style': 'display: none;'}, domHelper.createDom(goog.dom.TagName.SPAN, {'class': goog.ui.equation.TexPane.SECTION_TITLE_CSS_CLASS + ' ' + goog.ui.equation.TexPane.SECTION_TITLE_FLOAT_CSS_CLASS}, MSG_EE_TEX_EQUATION), this.getHelpUrl() ? domHelper.createDom(goog.dom.TagName.A, {'id': goog.ui.equation.TexPane.SECTION_LEARN_MORE_CSS_ID, 'target': '_blank', 'href': this.getHelpUrl()}, MSG_EE_LEARN_MORE) : null, domHelper.createDom(goog.dom.TagName.DIV, {'style': 'clear: both;'}), innerContainer = this.innerContainer_ = domHelper.createDom(goog.dom.TagName.DIV, {'style': 'position: relative'})); // Create menu palette. var menuPalette = this.paletteManager_.setActive( goog.ui.equation.Palette.Type.MENU); // Render the menu palette. menuPalette.render(innerContainer); innerContainer.appendChild(domHelper.createDom(goog.dom.TagName.DIV, {'style': 'clear:both'})); var texEdit = this.texEdit_ = domHelper.createDom('textarea', {'class': goog.ui.equation.TexPane.TEX_EDIT_CSS_CLASS, 'dir': 'ltr'}); innerContainer.appendChild(texEdit); innerContainer.appendChild( domHelper.createDom(goog.dom.TagName.DIV, {'class': goog.ui.equation.TexPane.SECTION_TITLE_CSS_CLASS}, MSG_EE_TEX_PREVIEW)); var previewContainer = this.previewContainer_ = domHelper.createDom( goog.dom.TagName.DIV, {'class': goog.ui.equation.TexPane.PREVIEW_CONTAINER_CSS_CLASS}); innerContainer.appendChild(previewContainer); this.setElementInternal(texEditorEl); }; /** @override */ goog.ui.equation.TexPane.prototype.enterDocument = function() { this.texInputHandler_ = new goog.events.InputHandler(this.texEdit_); // Listen to changes in the edit box to redraw equation. goog.events.listen(this.texInputHandler_, goog.events.InputHandler.EventType.INPUT, this.handleTexChange_, false, this); // Add a keyup listener for Safari that does not support the INPUT event, // and for users pasting with ctrl+v, which does not generate an INPUT event // in some browsers. this.getHandler().listen( this.texEdit_, goog.events.EventType.KEYDOWN, this.handleTexChange_); // Listen to the action event on the active palette. this.getHandler().listen(this.paletteManager_, goog.ui.equation.PaletteEvent.Type.ACTION, this.handlePaletteAction_); }; /** @override */ goog.ui.equation.TexPane.prototype.setVisible = function(visible) { goog.ui.equation.TexPane.base(this, 'setVisible', visible); if (visible) { goog.Timer.callOnce(this.focusTexEdit_, 0, this); } }; /** * Sets the focus to the TeX edit box. * @private */ goog.ui.equation.TexPane.prototype.focusTexEdit_ = function() { this.texEdit_.focus(); goog.dom.selection.setCursorPosition(this.texEdit_, this.texEdit_.value.length); }; /** * Handles input change within the TeX textarea. * @private */ goog.ui.equation.TexPane.prototype.handleEquationChange_ = function() { var text = this.getEquation(); if (text == this.lastRenderredText_) { return; // No change, no need to re-draw } this.lastRenderredText_ = text; var isEquationValid = !goog.ui.equation.ImageRenderer.isEquationTooLong(text); // Dispatch change so that dialog might update the state of its buttons. this.dispatchEvent( new goog.ui.equation.ChangeEvent( isEquationValid)); var container = this.previewContainer_; var dom = goog.dom.getDomHelper(container); dom.removeChildren(container); if (text) { var childNode; if (isEquationValid) { // Show equation image. var imgSrc = goog.ui.equation.ImageRenderer.getImageUrl(text); childNode = dom.createDom(goog.dom.TagName.IMG, {'src': imgSrc}); } else { // Show a warning message. /** * @desc A warning message shown when equation the user entered is too * long to display. */ var MSG_EE_TEX_EQUATION_TOO_LONG = goog.getMsg('Equation is too long'); childNode = dom.createDom(goog.dom.TagName.DIV, {'class': goog.ui.equation.TexPane.WARNING_CLASS}, MSG_EE_TEX_EQUATION_TOO_LONG); } dom.appendChild(container, childNode); } }; /** * Handles a change to the equation text. * Queues a request to handle input change within the TeX textarea. * Refreshing the image is done only after a short timeout, to combine * fast typing events into one draw. * @param {goog.events.Event} e The keyboard event. * @private */ goog.ui.equation.TexPane.prototype.handleTexChange_ = function(e) { this.changeSequence_++; goog.Timer.callOnce( goog.bind(this.handleTexChangeTimer_, this, this.changeSequence_), 500); }; /** * Handles a timer timeout on delayed text change redraw. * @param {number} seq The change sequence number when the timer started. * @private */ goog.ui.equation.TexPane.prototype.handleTexChangeTimer_ = function(seq) { // Draw only if this was the last change. If not, just wait for the last. if (seq == this.changeSequence_) { this.handleEquationChange_(); } }; /** * Handles an action generated by a palette click. * @param {goog.ui.equation.PaletteEvent} e The event object. * @private */ goog.ui.equation.TexPane.prototype.handlePaletteAction_ = function(e) { var palette = e.getPalette(); var paletteManager = this.paletteManager_; var activePalette = paletteManager.getActive(); var texEdit = this.texEdit_; // This is a click on the menu palette. if (palette.getType() == goog.ui.equation.Palette.Type.MENU) { var idx = palette.getHighlightedIndex(); var action = (idx != -1) ? palette.getAction(idx) : null; // Current palette is not menu. This means there's a palette popping up. if (activePalette != palette && activePalette.getType() == action) { // Deactivate the palette. paletteManager.deactivateNow(); return; } // We are clicking on the menu palette and there's no sub palette opening. // Then we just open the one corresponding to the item under the mouse. if (action) { var subPalette = this.paletteManager_.setActive( /** @type {goog.ui.equation.Palette.Type} */ (action)); if (!subPalette.getElement()) { subPalette.render(this.innerContainer_); } var el = subPalette.getElement(); goog.style.setPosition(el, 0, - el.clientHeight); } } else { activePalette = this.paletteManager_.getActive(); var action = activePalette.getAction(activePalette.getHighlightedIndex()); // If the click is on white space in the palette, do nothing. if (!action) { return; } // Do actual insert async because IE8 does not move the selection // position and inserts in the wrong place if called in flow. // See bug 2066876 goog.Timer.callOnce(goog.bind(this.insert_, this, action + ' '), 0); } // Let the tex editor always catch the focus. texEdit.focus(); }; /** * Inserts text into the equation at the current cursor position. * Moves the cursor to after the inserted text. * @param {string} text Text to insert. * @private */ goog.ui.equation.TexPane.prototype.insert_ = function(text) { var texEdit = this.texEdit_; var pos = goog.dom.selection.getStart(texEdit); var equation = texEdit['value']; equation = equation.substring(0, pos) + text + equation.substring(pos); texEdit['value'] = equation; goog.dom.selection.setCursorPosition(texEdit, pos + text.length); this.handleEquationChange_(); }; /** @override */ goog.ui.equation.TexPane.prototype.getEquation = function() { return this.texEdit_['value']; }; /** @override */ goog.ui.equation.TexPane.prototype.setEquation = function(equation) { this.texEdit_['value'] = equation; this.handleEquationChange_(); }; /** @override */ goog.ui.equation.TexPane.prototype.disposeInternal = function() { this.texInputHandler_.dispose(); this.paletteManager_ = null; goog.ui.equation.TexPane.base(this, 'disposeInternal'); };