mangroveorg/datawinners

View on GitHub
datawinners/media/javascript/jquery.highlighttextarea.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * jQuery highlightTextarea 2.0
 *
 * Copyright 2012, Damien "Mistic" Sorel
 *    http://www.strangeplanet.fr
 *
 * thanks to Julien L for the main algorythm
 *    http://stackoverflow.com/a/7599199
 *
 * thanks to Pascal Wacker for jQuery wrapper and API methods
 *    pascal.wacker@tilllate.com
 *
 * Dual licensed under the MIT or GPL Version 3 licenses.
 *    http://www.opensource.org/licenses/mit-license.php
 *    http://www.gnu.org/licenses/gpl.html
 *
 * Depends:
 *      jquery.js
 *    jquery-ui.js | resizable (optional)
 */
  

(function($) {
    /**
     * Plugin declaration
     */
    $.fn.highlightTextarea = function(options) {
        // callable public methods
        var callable = ['highlight','enable','disable','setOptions','setWords'];
        
        var plugin = $(this).data('highlightTextarea');
        
        // already instantiated and trying to execute a method
        if (plugin && typeof options === 'string') {
            if ($.inArray(options,callable)!==false) {
                return plugin[options].apply(plugin, Array.prototype.slice.call(arguments, 1));
            }
            else {
                throw 'Method "' + options + '" does not exist on jQuery.highlightTextarea';
            }
        }
        // not instantiated and trying to pass options object (or nothing)
        else if (!plugin && (typeof options === 'object' || !options)) {
            if (!options) {
                options = {};
            }
            
            // extend defaults
            options = $.extend({}, $.fn.highlightTextarea.defaults, options);
            options.regParam = options.caseSensitive ? 'g' : 'gi';

            // for each element instantiate the plugin
            return this.each(function() {
                var plugin = $(this).data('highlightTextarea');

                // create new instance of the plugin if the plugin isn't initialised
                if (!plugin) {
                    plugin = new $.highlightTextarea($(this), options);
                    plugin.init();
                    $(this).data('highlightTextarea', plugin);
                }
            });
        }
    }
    
    /**
     * Defaults
     */
    $.fn.highlightTextarea.defaults = {
        words:         [],
        color:         '#ffff00',
        caseSensitive: true,
        resizable:     false,
        id:            null,
        debug:         false
    };

    /**
     * Main plugin function
     */
    $.highlightTextarea = function(element, options) {
        this.options = options;
        
        if (element instanceof jQuery) {
            this.$textarea = element;
        }
        else {
            this.$textarea = $(element);
        }
        
        this.$main = null;
        this.$highlighterContainer = null;
        this.$highlighter = null;
        
        
        /*
         * init the plugin
         * scope: private
         */
        this.init = function() {
            // build the HTML wrapper
            if (this.$textarea.closest('.highlightTextarea').length <= 0) {
                this.$textarea.wrap('<div class="highlightTextarea" />');
            }
            this.$main = this.$textarea.parent('.highlightTextarea');

            if (this.$main.find('.highlighterContainer').length <= 0) {
                this.$main.prepend('<div class="highlighterContainer"></div>');
            }
            this.$highlighterContainer = this.$main.children('.highlighterContainer');
            
            if (this.$highlighterContainer.find('.highlighter').length <= 0) {
                this.$highlighterContainer.html('<div class="highlighter"></div>');
            }
            this.$highlighter = this.$highlighterContainer.children('.highlighter');

            // set id
            if (this.options.id != null) {
                this.$main.attr('id', this.options.id);
            }

            // set css
            this.updateCss();

            // bind the events
            this.bindEvents();

            // apply the resizeable
            this.applyResizable();

            // highlight content
            this.highlight();
        }

        /*
         * compute highlight
         * @param delay: boolean - use a delayed update
         * scope: public
         */
        this.highlight = function(delay) {
            if (delay==null || delay==false) {
                this.applyText(this.$textarea.val());
            }
            else {
                this.condensator($.proxy(function(){ 
                  this.applyText(this.$textarea.val()); 
                }, this), 100, 300);
            }
            
            return this.$textarea.data('highlightTextareaEvents')===true;
        }

        /*
         * update plugin options
         * scope: public
         */
        this.setOptions = function(options) {
            if (typeof options != 'object') {
                options = {};
            }
            
            this.options = $.extend({}, this.options, options);
            this.options.regParam = this.options.caseSensitive ? 'g' : 'gi';
            
            if (this.options.debug) {
                this.$highlighter.addClass('debug');
            }
            else {
                this.$highlighter.removeClass('debug');
            }
            
            if (this.$textarea.data('highlightTextareaEvents')===true) {
                this.highlight();
                return true;
            }
            else {
                return false;
            }
        }

        /*
         * update words list
         * scope: public
         */
        this.setWords = function(words) {
            if (typeof words !== 'string' && !(words instanceof Array)) {
                words = [];
            }
            else if (typeof words === 'string') {
                words = [words];
            }
            this.options.words = words;
            
            if (this.$textarea.data('highlightTextareaEvents')===true) {
                this.highlight();
                return true;
            }
            else {
                return false;
            }
        }
        
        /*
         * add events handlers
         * scope: private
         */
        this.bindEvents = function() {
            var events = this.$textarea.data('highlightTextareaEvents');
            
            if (typeof events != 'boolean' || events !== true) {
                // prevend positionning errors by allways focusing the textarea
                this.$highlighter.on({
                  'click.highlightTextarea' : $.proxy(function(){ this.$textarea.focus(); }, this)
                });
                
                // add triggers to textarea
                this.$textarea.on({
                    'input.highlightTextarea' :  $.proxy(function(){ this.highlight(true); }, this),
                    'resize.highlightTextarea' : $.proxy(function(){ this.updateSizePosition(true); }, this),
                    'scroll.highlightTextarea' : $.proxy(function(){ this.updateSizePosition(); }, this)
                });

                this.$textarea.data('highlightTextareaEvents', true);
            }
        }
          
        /*
         * remove event handlers
         * scope: private
         */
        this.unbindEvents = function() {
            this.$highlighter.off('click.highlightTextarea');
            this.$textarea.off('input.highlightTextarea scroll.highlightTextarea resize.highlightTextarea');
            this.$textarea.data('highlightTextareaEvents', false);
        }
        
        /*
         * enable the highlighting
         * scope: public
         */
        this.enable = function() {
            this.bindEvents();
            this.highlight();
        }
        
        /*
         * disable the highlighting
         * scope: public
         */
        this.disable = function() {
            this.unbindEvents();
            this.$highlighter.html(this.html_entities(this.$textarea.val()));
        }

        /*
         * set style of containers
         * scope: private
         */
        this.updateCss = function() {
            // the main container has the same size and position than the original textarea
            this.cloneCss(this.$textarea, this.$main, [
                'float','vertical-align'
            ]);
            this.$main.css({
                'width':  this.$textarea.outerWidth(true),
                'height': this.$textarea.outerHeight(true)
            });
            
            // the highlighter container is positionned at "real" top-left corner of the textarea and takes its background
            this.cloneCss(this.$textarea, this.$highlighterContainer, [
                'background','background-image','background-color','background-position','background-repeat','background-origin','background-clip','background-size',
                'padding-top','padding-right','padding-bottom','padding-left'
            ]);
            this.$highlighterContainer.css({
                'top':    this.toPx(this.$textarea.css('margin-top')) + this.toPx(this.$textarea.css('border-top-width')),
                'left':   this.toPx(this.$textarea.css('margin-left')) + this.toPx(this.$textarea.css('border-left-width')),
                'width':  this.$textarea.width(),
                'height': this.$textarea.height()
            });
            
            // the highlighter has the same size than the "inner" textarea and must have the same font properties
            this.cloneCss(this.$textarea, this.$highlighter, [
                'font-size','font-family','font-style','font-weight','line-height',
                'vertical-align','word-spacing','text-align'
            ]);
            this.$highlighter.css({
                'width':  this.$textarea.width(),
                'height': this.$textarea.height()
            });
            
            // now make the textarea transparent to see the highlighter throught
            this.$textarea.css({
                'background': 'none',
            });
            
            // display highlighter text for debuging
            if (this.options.debug) {
                this.$highlighter.addClass('debug');
            }
        }
        
        /*
         * set textarea as resizable
         * scope: private
         */
        this.applyResizable = function() {
            if (this.options.resizable && jQuery.ui) {
                this.$textarea.resizable({
                    'handles': 'se',
                    'resize':  $.proxy(function() { this.updateSizePosition(true); }, this)
                });
            }
        }

        /*
         * replace $highlighter html with formated $textarea content
         * scope: private
         */
        this.applyText = function(text) {
            text = this.html_entities(text);
            
            if (this.options.words.length > 0) {
                replace = new Array();
                
                for (var i=0; i<this.options.words.length; i++) {
                  replace.push(this.html_entities(this.options.words[i]));
                }
                
                text = text.replace(
                  new RegExp('('+replace.join('|')+')', this.options.regParam), 
                  "<span class=\"highlight\" style=\"background-color:"+this.options.color+";\">$1</span>"
                );
            }
            
            this.$highlighter.html(text);
            this.updateSizePosition();
        }

        /*
         * adapt $highlighter size and position according to $textarea size and scroll bar
         * @param forced: boolean - update containers size
         * scope: private
         */
        this.updateSizePosition = function(forced) {
            // resize containers
            if (forced) {
                this.$main.css({
                    'width':  this.$textarea.outerWidth(true),
                    'height': this.$textarea.outerHeight(true)
                });
                this.$highlighterContainer.css({
                    'width':  this.$textarea.width(),
                    'height': this.$textarea.height()
                });
            }
            
            if (
              (this.$textarea[0].clientHeight < this.$textarea[0].scrollHeight && this.$textarea.css('overflow') != 'hidden' && this.$textarea.css('overflow-y') != 'hidden')
              || this.$textarea.css('overflow') == 'scroll' || this.$textarea.css('overflow-y') == 'scroll'
            ) {
                var padding = 18;
            }
            else {
                var padding = 5;
            }
            
            this.$highlighter.css({
                'width':         this.$textarea.width()-padding,
                'height':        this.$textarea.height()+this.$textarea.scrollTop(),
                'padding-right': padding,
                'top':           -this.$textarea.scrollTop()
            });
        }

        /*
         * set 'to' css attributes listed in 'what' as defined for 'from'
         * scope: private
         */
        this.cloneCss = function(from, to, what) {
            for (var i=0; i<what.length; i++) {
                to.css(what[i], from.css(what[i]));
            }
        }

        /*
         * clean/convert px and em size to px size (without 'px' suffix)
         * scope: private
         */
        this.toPx = function(value) {
            if (value != value.replace('em', '')) {
                // https://github.com/filamentgroup/jQuery-Pixel-Em-Converter
                var that = parseFloat(value.replace('em', '')),
                    scopeTest = $('<div style="display:none;font-size:1em;margin:0;padding:0;height:auto;line-height:1;border:0;">&nbsp;</div>').appendTo('body'),
                    scopeVal = scopeTest.height();
                scopeTest.remove();
                return Math.round(that * scopeVal);
            }
            else if (value != value.replace('px', '')) {
                return parseInt(value.replace('px', ''));
            }
            else {
                return parseInt(value);
            }
        }
        
        /*
         * apply html entities
         * scope: private
         */
        this.html_entities = function(value) {
            if (value) {
                return $('<div />').text(value).html();
            }
            else {
                return '';
            }
        }
        
        /*
         * add a delay with age limit to a method
         * scope: private
         */
        var timer = null;
        var startTime = null;
        this.condensator = function(callback, ms, limit) {
            if (limit==null) {
              limit=ms;
            }
            
            var date = new Date();
            clearTimeout(timer);
            
            if (startTime==null) {
                startTime = date.getTime();
            }
            
            if (date.getTime() - startTime > limit) {
                callback.call();
                startTime = date.getTime();
            }
            else {
                timer = setTimeout(callback, ms);
            }
        }
    };
})(jQuery);