/*! * jQuery highlightTextarea * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) * Licensed under MIT (http://opensource.org/licenses/MIT) */ //= require jquery (function($){ "use strict"; var mouseOverElement = null; var isNumeric = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; // Highlighter CLASS DEFINITON // =============================== var Highlighter = function($el, options) { // global variables this.settings = $.extend({}, Highlighter.DEFAULTS); this.scrollbarWidth = Utilities.getScrollbarWidth(); this.isInput = $el[0].tagName.toLowerCase()=='input'; this.active = false; this.matches = []; // build HTML this.$el = $el; this.$el.wrap('
'); this.$main = this.$el.parent(); this.$main.prepend('
'); this.$container = this.$main.children().first(); this.$highlighter = this.$container.children(); this.setOptions(options); // set id if (this.settings.id) { this.$main[0].id = this.settings.id; } // resizable if (this.settings.resizable) { this.applyResizable(); } // run this.updateCss(); this.bindDocumentEvents(); this.bindEvents(); this.highlight(); }; Highlighter.DEFAULTS = { words: {}, ranges: {}, color: '#ffff00', caseSensitive: true, wordsOnly: false, resizable: false, resizableOptions: {}, id: '', debug: false }; // PUBLIC METHODS // =============================== /* * Refresh highlight */ Highlighter.prototype.highlight = function() { var text = this.$el.val(), that = this; that.spacer = ''; if (this.settings.wordsOnly ) { that.spacer = '\\b'; } function htmlDecode(input) { return $('
').text(input).html(); } // Encode text before inserting into
so that the textarea and // overlay don't get out if sync when the textarea contains something // HTML (e.g. "&" or ). text = htmlDecode(text); var matches = []; $.each(this.settings.words, function(color, words) { var wordsRe = htmlDecode(words.join("|")); var re = that.spacer + '(' + wordsRe + ')' + that.spacer; var regex = new RegExp(re, that.regParam); var wordMatches = text.match(regex); if (wordMatches) { var evaluated = []; $.each(words, function(index, match) { match = htmlDecode(match); matches.push(match); if (evaluated.indexOf(match) === -1) { text = text.replace( new RegExp(match, that.regParam), function(innerMatch, start, contents) { var encodedMatch = innerMatch .replace(/[&"<>]/g, function (c) { return { '&': "&", '"': """, '<': "<", '>': ">" }[c]; }); return '' + encodedMatch + ''; } ); evaluated.push(match); } }); } }); $.each(this.settings.ranges, function(i, range) { if (range.start < text.length) { text = Utilities.strInsert(text, range.end, ''); var mark = '= left && mouseY >= top && mouseX <= left + width && mouseY <= top + height) { mouseOverElement = this; return; } }); if(mouseOverElement != lastMouseOverElement) { if(lastMouseOverElement != null) { $(lastMouseOverElement).trigger('mouseout'); } if(mouseOverElement != null) { $(mouseOverElement).trigger('mouseover'); } } }); Highlighter.documentEventsBound = true; }; /* * Attach event listeners */ Highlighter.prototype.bindEvents = function() { if (this.active) { return; } this.active = true; var that = this; // prevent positioning errors by always focusing the textarea this.$highlighter.bind({ 'this.highlighter': function() { that.$el.focus(); } }); // add triggers to textarea this.$el.bind({ 'input.highlightTextarea': Utilities.throttle(function() { this.highlight(); }, 100, this), 'resize.highlightTextarea': Utilities.throttle(function() { this.updateSizePosition(true); }, 50, this), 'scroll.highlightTextarea select.highlightTextarea': Utilities.throttle(function() { this.updateSizePosition(); }, 50, this) }); if (this.isInput) { this.$el.bind({ // Prevent Cmd-Left Arrow and Cmd-Right Arrow on Mac strange behavior 'keydown.highlightTextarea keypress.highlightTextarea keyup.highlightTextarea': function() { setTimeout($.proxy(that.updateSizePosition, that), 1); }, // Force Chrome behavior on all browsers: reset input position on blur 'blur.highlightTextarea': function() { this.value = this.value; this.scrollLeft = 0; that.updateSizePosition.call(that); } }); } }; /* * Detach event listeners */ Highlighter.prototype.unbindEvents = function() { if (!this.active) { return; } this.active = false; this.$highlighter.off('.highlightTextarea'); this.$el.off('.highlightTextarea'); }; /* * Update CSS of wrapper and containers */ Highlighter.prototype.updateCss = function() { // the main container has the same size and position than the original textarea Utilities.cloneCss(this.$el, this.$main, [ 'float','vertical-align' ]); this.$main.css({ 'width': this.$el.outerWidth(true), 'height': this.$el.outerHeight(true) }); // the highlighter container is positionned at "real" top-left corner of the textarea and takes its background Utilities.cloneCss(this.$el, this.$container, [ 'background-image','background-color','background-position','background-repeat', 'background-origin','background-clip','background-size','background-attachment', 'padding-top','padding-right','padding-bottom','padding-left' ]); this.$container.css({ 'top': Utilities.toPx(this.$el.css('margin-top')) + Utilities.toPx(this.$el.css('border-top-width')), 'left': Utilities.toPx(this.$el.css('margin-left')) + Utilities.toPx(this.$el.css('border-left-width')), 'width': this.$el.width(), 'height': this.$el.height() }); // the highlighter has the same size than the "inner" textarea and must have the same font properties Utilities.cloneCss(this.$el, this.$highlighter, [ 'font-size','font-family','font-style','font-weight','font-variant','font-stretch', 'vertical-align','word-spacing','text-align','letter-spacing', 'text-rendering' ]); // now make the textarea transparent to see the highlighter through this.$el.css({ 'background': 'none' }); }; /* * Apply jQueryUi Resizable if available */ Highlighter.prototype.applyResizable = function() { if (jQuery.ui) { var resizableOptionsDefaults = { 'handles': 'se', 'resize': Utilities.throttle(function() { this.updateSizePosition(true); }, 50, this) }; var resizableOptions = $.extend({}, resizableOptionsDefaults, this.settings.resizableOptions); this.$el.resizable(resizableOptions); } }; /* * Update size and position of the highlighter * @param forced {boolean} true to resize containers */ Highlighter.prototype.updateSizePosition = function(forced) { // resize containers if (forced) { this.$main.css({ 'width': this.$el.outerWidth(true), 'height': this.$el.outerHeight(true) }); this.$container.css({ 'width': this.$el.width(), 'height': this.$el.height() }); } var padding = 0, width; if (!this.isInput) { // account for vertical scrollbar width if (this.$el.css('overflow') == 'scroll' || this.$el.css('overflow-y') == 'scroll' || ( this.$el.css('overflow') != 'hidden' && this.$el.css('overflow-y') != 'hidden' && this.$el[0].clientHeight < this.$el[0].scrollHeight ) ) { padding = this.scrollbarWidth; } width = this.$el.width()-padding; } else { // TODO: There's got to be a better way of going about this than just using 99999px... width = 99999; } this.$highlighter.css({ 'width': width, 'height': this.$el.height() + this.$el.scrollTop(), 'top': -this.$el.scrollTop(), 'left': -this.$el.scrollLeft() }); }; // Utilities CLASS DEFINITON // =============================== var Utilities = function(){}; /* * Get the scrollbar with on this browser */ Utilities.getScrollbarWidth = function() { var parent = $('
 
').appendTo('body'), child = parent.children(), width = child.innerWidth() - child.height(100).innerWidth(); parent.remove(); return width; }; /* * Copy a list of CSS properties from one object to another * @param from {jQuery} * @param to {jQuery} * @param what {string[]} */ Utilities.cloneCss = function(from, to, what) { for (var i=0, l=what.length; i 
').appendTo('body'); value = Math.round(parseFloat(value.replace('em', '')) * el.height()); el.remove(); return value; } else if (value != value.replace('px', '')) { return parseInt(value.replace('px', '')); } else { return parseInt(value); } }; /* * Converts HTMl entities * @param str {string} * @return {string} */ Utilities.htmlEntities = function(str) { if (str) { return $('
').text(str).html(); } else { return ''; } }; /* * Inserts a string in another string at given position * @param string {string} * @param index {int} * @param value {string} * @return {string} */ Utilities.strInsert = function(string, index, value) { return string.slice(0, index) + value + string.slice(index); }; /* * Apply throttling to a callback * @param callback {function} * @param delay {int} milliseconds * @param context {object|null} * @return {function} */ Utilities.throttle = function(callback, delay, context) { var state = { pid: null, last: 0 }; return function() { var elapsed = new Date().getTime() - state.last, args = arguments, that = this; function exec() { state.last = new Date().getTime(); if (context) { return callback.apply(context, Array.prototype.slice.call(args)); } else { return callback.apply(that, Array.prototype.slice.call(args)); } } if (elapsed > delay) { return exec(); } else { clearTimeout(state.pid); state.pid = setTimeout(exec, delay - elapsed); } }; }; /* * Formats a list of words into a hash of arrays (Color => Words list) * @param words {mixed} * @param color {string} default color * @return {object[]} */ Utilities.cleanWords = function(words, color) { var out = {}; if (!$.isArray(words)) { words = [words]; } for (var i=0, l=words.length; i Ranges list) * @param ranges {mixed} * @param color {string} default color * @return {object[]} */ Utilities.cleanRanges = function(ranges, color) { var out = []; if ($.isPlainObject(ranges) || isNumeric(ranges[0])) { ranges = [ranges]; } for (var i=0, l=ranges.length; i= range.end) { $.error('Invalid range end/start'); } if (range.start < current) { $.error('Ranges overlap'); } current = range.end; }); out.reverse(); return out; }; // JQUERY PLUGIN DEFINITION // =============================== $.fn.highlightTextarea = function(option) { var args = arguments; return this.each(function() { var $this = $(this), data = $this.data('highlighter'), options = typeof option == 'object' && option; if (!data && option == 'destroy') { return; } if (!data) { data = new Highlighter($this, options); $this.data('highlighter', data); } if (typeof option == 'string') { data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; }(window.jQuery || window.Zepto));