TextPane (a TextArea which expands to fill a pane)

tliebeck's picture

This is a text area component that expands to fill a pane. Resizing the pane will grow/shrink the text area appropriately. It's not 100% finished at the moment (border size calculation doesn't work properly with borders with variable heights, for example).

Contrib.TextPane = Core.extend(Echo.Component, {

    $load: function() {
        Echo.ComponentFactory.registerType("Contrib.TextPane", this);
    },

    componentType: "Contrib.TextPane",
    focusable: true,
    pane: true,
    
    /**
     * Programatically performs a text component action.
     */
    doAction: function() {
        this.fireEvent({type: "action", source: this, actionCommand: this.get("actionCommand")});
    }    
});

/**
 * Component rendering peer: TextComponent
 */
Contrib.TextPane.Sync = Core.extend(Echo.Render.ComponentSync, {
    
    $static: {
        _supportedPartialProperties: ["text"]
    },
    
    $load: function() {
        Echo.Render.registerPeer("Contrib.TextPane", this);
    },

    _div: null,
    _textArea: null,
    
    _processBlur: function(e) {
        if (!this.client.verifyInput(this.component, Echo.Client.FLAG_INPUT_PROPERTY)) {
            return;
        }
        this.sanitizeInput();
        this.component.set("text", e.registeredTarget.value);
    },
    
    _processClick: function(e) {
        if (!this.client.verifyInput(this.component, Echo.Client.FLAG_INPUT_PROPERTY)) {
            return;
        }
        this.component.application.setFocusedComponent(this.component);
    },

    _processKeyPress: function(e) {
        if (!this.client.verifyInput(this.component, Echo.Client.FLAG_INPUT_PROPERTY)) {
            Core.Web.DOM.preventEventDefault(e);
            return true;
        }
    },
    
    _processKeyUp: function(e) {
        if (!this.client.verifyInput(this.component, Echo.Client.FLAG_INPUT_PROPERTY)) {
            Core.Web.DOM.preventEventDefault(e);
            return true;
        }
        this.sanitizeInput();
        
        // Store last updated text in local value, to ensure that we do not attempt to
        // reset it to this value in renderUpdate() and miss any characters that were
        // typed between repaints.
        this._text = e.registeredTarget.value;
        
        this.component.set("text", this._text);
        if (e.keyCode == 13) {
            this.component.doAction();
        }
        return true;
    },

    renderAdd: function(update, parentElement) {
        this._div = document.createElement("div");
        this._div.id = this.component.renderId;
        this._div.style.cssText = "position:absolute;top:0;left:0;bottom:0;right:0;overflow:hidden;";
        
        this._textArea = document.createElement("textarea");
        this._textArea.style.cssText = "margin:0;padding:0;border:0px none;width:100%;"
        this._div.appendChild(this._textArea);
        
        this._textArea.style.overflow = "auto";

        if (this.component.get("text")) {
            this._text = this._textArea.value = this.component.get("text");
        }
        
        var border = this.component.render("border");
        this._borderSizeVertical = Echo.Sync.Border.getPixelSize(border);

        if (this.component.isRenderEnabled()) {
            Echo.Sync.Border.render(border, this._div);
            Echo.Sync.Color.renderFB(this.component, this._textArea);
            Echo.Sync.Font.render(this.component.render("font"), this._textArea);
            Echo.Sync.FillImage.render(this.component.render("backgroundImage"), this._textArea);
        } else {
            Echo.Sync.Border.render(Echo.Sync.getEffectProperty(this.component, "border", "disabledBorder", true), 
                    this._div);
            Echo.Sync.Color.render(Echo.Sync.getEffectProperty(this.component, "foreground", "disabledForeground", true), 
                    this._textArea, "color");
            Echo.Sync.Color.render(Echo.Sync.getEffectProperty(this.component, "background", "disabledBackground", true), 
                    this._textArea, "backgroundColor");
            Echo.Sync.Font.render(Echo.Sync.getEffectProperty(this.component, "font", "disabledFont", true), 
                    this._textArea);
            Echo.Sync.FillImage.render(Echo.Sync.getEffectProperty(this.component, 
                    "backgroundImage", "disabledBackgroundImage", true), this._textArea);
        }        

        Core.Web.Event.add(this._textArea, "click", Core.method(this, this._processClick), false);
        Core.Web.Event.add(this._textArea, "blur", Core.method(this, this._processBlur), false);
        Core.Web.Event.add(this._textArea, "keypress", Core.method(this, this._processKeyPress), false);
        Core.Web.Event.add(this._textArea, "keyup", Core.method(this, this._processKeyUp), false);

        parentElement.appendChild(this._div);
    },

    renderDisplay: function() {
        Core.Web.VirtualPosition.redraw(this._div);
        var height = this._div.parentNode.offsetHeight - this._borderSizeVertical;
        if (height > 0) {
            this._textArea.style.height = height + "px";
        }
    },

    renderDispose: function(update) {
        Core.Web.Event.removeAll(this._textArea);
        this._textArea = null;
        this._div = null;
    },

    renderFocus: function() {
        Core.Web.DOM.focusElement(this._textArea);
    },

    renderUpdate: function(update) {
        var fullRender =  !Core.Arrays.containsAll(Echo.Sync.TextComponent._supportedPartialProperties, 
                    update.getUpdatedPropertyNames(), true);
    
        if (fullRender) {
            var element = this._textArea;
            var containerElement = element.parentNode;
            this.renderDispose(update);
            containerElement.removeChild(element);
            this.renderAdd(update, containerElement);
        } else {
            if (update.hasUpdatedProperties()) {
                var textUpdate = update.getUpdatedProperty("text");
                if (textUpdate && textUpdate.newValue != this._text) {
                    this._textArea.value = textUpdate.newValue == null ? "" : textUpdate.newValue;
                }
            }
        }
        
        // Store text in local value.
        this._text = this.component.get("text");
        
        return false; // Child elements not supported: safe to return false.
    },
    
    sanitizeInput: function() {
        var maximumLength = this.component.render("maximumLength", -1);
        if (maximumLength >= 0) {
            if (this._textArea.value && this._textArea.value.length > maximumLength) {
                this._textArea.value = this._textArea.value.substring(0, maximumLength);
            }
        }
    }
});