1 /**
   2  * CutterClasses is the config singleton for classes used in Cutter
   3  * @class CutterClasses
   4  * @constructor
   5  * @author Tomas Corral Casas
   6  * @version 1.0
   7  * @type Object
   8  */
   9 var CutterClasses = function()
  10 {
  11     this.more = "more";
  12 };
  13 /**
  14  * CutterTexts is the config singleton for texts used in Cutter
  15  * @class CutterTexts
  16  * @constructor
  17  * @author Tomas Corral Casas
  18  * @version 1.0
  19  * @type Object
  20  */
  21 var CutterTexts = function()
  22 {
  23     this.more =  "View more";
  24 };
  25 /**
  26  * Cutter is a class that allows HTML code to cut a number of words contained in the nodes, keeping intact the HTML markup.
  27  * @author Tomas Corral Casas
  28  * @version 1.0
  29  * @class Cutter
  30  * @constructor
  31  * @type Object
  32  */
  33 var Cutter = function()
  34 {
  35     /**
  36      * oApplyTo is the Dom object where we want to use the cutter.
  37      * @member Cutter.prototype
  38      * @author Tomas Corral Casas
  39 	 * @version 1.0
  40      * @type Object
  41      */
  42     this.oApplyTo = null;
  43     /**
  44      * oBackupApplyTo is the clone from Dom object to get it when showing the content.
  45      * @member Cutter.prototype
  46      * @author Tomas Corral Casas
  47 	 * @version 1.0
  48      * @type Object
  49      */
  50     this.oBackupApplyTo = null;
  51     /**
  52      * oTarget is the Dom object where to put the cutted code
  53      * @member Cutter.prototype
  54      * @author Tomas Corral Casas
  55 	 * @version 1.0
  56      * @type Object
  57      */
  58     this.oTarget = null;
  59     /**
  60      * oClasses is the config singleton for classes used in Cutter
  61      * @member Cutter.prototype
  62      * @author Tomas Corral Casas
  63 	 * @version 1.0
  64      * @type Object
  65      */
  66     this.oClasses = new CutterClasses();
  67     /**
  68      * oTexts is the config singleton for texts used in Cutter
  69      * @member Cutter.prototype
  70      * @author Tomas Corral Casas
  71 	 * @version 1.0
  72      * @type Object
  73      */
  74     this.oTexts = new CutterTexts();
  75     /**
  76      * nWords is the number of words to Cut
  77      * @member Cutter.prototype
  78      * @author Tomas Corral Casas
  79 	 * @version 1.0
  80      * @type Number
  81      */
  82     this.nWords = 0;
  83     /**
  84      * nWordsCounter is the counter of words when finding them in code
  85      * @member Cutter.prototype
  86      * @author Tomas Corral Casas
  87 	 * @version 1.0
  88      * @type Number
  89      */
  90     this.nWordsCounter = 0;
  91     /**
  92      * oViewMore is a reference to the "see more" link.
  93      * @member Cutter.prototype
  94      * @author Tomas Corral Casas
  95 	 * @version 1.0
  96      * @type Object
  97      */
  98     this.oViewMore = null;
  99     /**
 100      * oSerialized is the JSON object where Cutter serializes all the DOM objects inside the oApplyTo Dom element
 101      * @member Cutter.prototype
 102      * @author Tomas Corral Casas
 103 	 * @version 1.0
 104      * @type Object
 105      */
 106     this.oSerialized = {};
 107     /**
 108      * oDocumentFragment is the DocumentFragment where the Dom elements are inserted before insert on Target
 109      * @member Cutter.prototype
 110      * @author Tomas Corral Casas
 111 	 * @version 1.0
 112      * @type Object
 113      */
 114     this.oDocumentFragment = document.createDocumentFragment();
 115 	/**
 116 	 * bTest is a property to now if you want to test the Cutter class
 117 	 * This is used to change the type of id for each element.
 118      * false by default.
 119      * @member Cutter.prototype
 120      * @author Tomas Corral Casas
 121 	 * @version 1.0
 122      * @type Boolean
 123 	 */
 124     this.bTest = false;
 125     /**
 126      * nIdTest is the property for testing that will save the order for id.
 127      * 0 by default.
 128      * @member Cutter.prototype
 129      * @author Tomas Corral Casas
 130 	 * @version 1.0
 131      * @type Number
 132      */
 133     this.nIdTest = 0;
 134 	/**
 135 	 * bNeedViewMore is a property that will check it it's necessary to add the link to view more or not
 136 	 * It checks if its necessary if for some reason the content is cutted.
 137 	 * false by default.
 138      * @member Cutter.prototype
 139      * @author Tomas Corral Casas
 140 	 * @version 1.0
 141      * @type Boolean
 142 	 */
 143 	this.bNeedViewMore = false;
 144 	/**
 145 	 * bNotViewMore is a property that could be setted by the user to add or not the link to view more content if needed.
 146 	 * false by default.
 147      * @member Cutter.prototype
 148      * @author Tomas Corral Casas
 149 	 * @version 1.0
 150      * @type Boolean
 151 	 */
 152 	this.bNotViewMore = false;
 153 };
 154 /**
 155  * applyTo is the method that sets the Dom object where to apply the cutter
 156  * @member Cutter.prototype
 157  * @author Tomas Corral Casas
 158  * @version 1.0
 159  * @param  {object} oApplyTo This is the Dom element
 160  * @return the instance of the Cutter
 161  * @type Object
 162  */
 163 Cutter.prototype.applyTo = function(oApplyTo)
 164 {
 165     if(!oApplyTo)
 166     {
 167     	return this;
 168     }
 169     this.oApplyTo = oApplyTo;
 170     this.oBackupApplyTo = oApplyTo.cloneNode(true);
 171     return this;
 172 };
 173 /**
 174  * setTarget is the method that sets the Dom object where to put the cutted code
 175  * @member Cutter.prototype
 176  * @author Tomas Corral Casas
 177  * @version 1.0
 178  * @param  {object} oTarget This is the Dom element
 179  * @return the instance of the Cutter
 180  * @type Object
 181  */
 182 Cutter.prototype.setTarget = function(oTarget)
 183 {
 184     if(!oTarget)
 185     {
 186     	return this;
 187     }
 188     this.oTarget = oTarget;
 189     return this;
 190 };
 191 /**
 192  * setClasses is the method that sets the config singleton of Classes used in Cutter
 193  * @member Cutter.prototype
 194  * @author Tomas Corral Casas
 195  * @version 1.0
 196  * @param  {object} oClasses This is the singleton config
 197  * @return the instance of the Cutter
 198  * @type Object
 199  */
 200 Cutter.prototype.setClasses = function(oClasses)
 201 {
 202     if(!oClasses)
 203     {
 204     	return this;
 205     }
 206     this.oClasses = oClasses;
 207     return this;
 208 };
 209 /**
 210  * setTexts is the method that sets the config singleton of Texts used in Cutter
 211  * @member Cutter.prototype
 212  * @author Tomas Corral Casas
 213  * @version 1.0
 214  * @param  {object} oTexts This is the singleton config
 215  * @return the instance of the Cutter
 216  * @type Object
 217  */
 218 Cutter.prototype.setTexts = function(oTexts)
 219 {
 220     if(!oTexts)
 221     {
 222     	return this;
 223     }
 224     this.oTexts = oTexts;
 225     return this;
 226 };
 227 /**
 228  * setWords is the method used to set the max number of words before cut the code.
 229  * @member Cutter.prototype
 230  * @author Tomas Corral Casas
 231  * @version 1.0
 232  * @param  {number} nWords This is the number of words to see.
 233  * @return the instance of the Cutter
 234  * @type Object
 235  */
 236 Cutter.prototype.setWords = function(nWords)
 237 {
 238     if(!nWords)
 239     {
 240     	return this;
 241     }
 242     this.nWords = nWords - 1;
 243     return this;
 244 };
 245 /**
 246  * _trim is an utilities method used to keep out all the spaces before or after the sentence
 247  * @member Cutter.prototype
 248  * @author Tomas Corral Casas
 249  * @version 1.0
 250  * @param  {string} sString This is the text to be trimmed
 251  * @private
 252  * @return the trimmed string
 253  * @type String
 254  */
 255 Cutter.prototype._trim = function(sString)
 256 {
 257     return sString.replace(/^\s+/g, '').replace(/\s+$/g, '');
 258 };
 259 /**
 260  * _countWords is an utilities method used to count the words in a String
 261  * @member Cutter.prototype
 262  * @author Tomas Corral Casas
 263  * @version 1.0
 264  * @param  {string} sText The text where to know how many words are.
 265  * @private
 266  * @return the number of words in the string
 267  * @type Number
 268  */
 269 Cutter.prototype._countWords = function(sText)
 270 {
 271     return this._trim(sText).split(" ").length;
 272 };
 273 /**
 274  * _getOnlyNumberOfWords is an utilities method used to get a number of words from the string
 275  * @member Cutter.prototype
 276  * @author Tomas Corral Casas
 277  * @version 1.0
 278  * @param  {string} sString The text from which to extract the words
 279  * @param  {number} nWords The number of words to get
 280  * @private
 281  * @return the number of words in the string
 282  * @type Number
 283  */
 284 Cutter.prototype._getOnlyNumberOfWords = function(sString, nWords)
 285 {
 286     return this._trim(sString).split(" ").splice(0, nWords).join(" ");
 287 };
 288 /**
 289  * createViewMore is the method that creates the link to see all the content again
 290  * @member Cutter.prototype
 291  * @author Tomas Corral Casas
 292  * @version 1.0
 293  * @private
 294  */
 295 Cutter.prototype.createViewMore = function()
 296 {
 297 	var oLink = document.createElement("a");
 298 	oLink.className = this.oClasses.more;
 299 	oLink.title = this.oTexts.more;
 300 	oLink.innerHTML = this.oTexts.more;
 301 	this.oViewMore = oLink;
 302 };
 303 /**
 304  * _getFirstElementOfObject is an utilities method used to get the first element in a JSON object
 305  * @member Cutter.prototype
 306  * @author Tomas Corral Casas
 307  * @version 1.0
 308  * @param  {object} oObject The JSON object from which to obtain the first element
 309  * @private
 310  * @return the first element in the JSON object
 311  * @type Object
 312  */
 313 Cutter.prototype._getFirstElementOfObject = function(oObject)
 314 {
 315     var oFirstElement = undefined;
 316     for (var sKey in oObject)
 317     {
 318         if (oObject.hasOwnProperty(sKey))
 319         {
 320             oFirstElement = oObject[sKey];
 321             break;
 322         }
 323     }
 324     return oFirstElement;
 325 };
 326 /**
 327  * deserializeObject is an utilities method used to deserialize a JSON object in a Dom element
 328  * @member Cutter.prototype
 329  * @author Tomas Corral Casas
 330  * @version 1.0
 331  * @param  {object} oSerialized The JSON object from which to obtain the Dom element information
 332  * @param  {object} oParent The Dom element where to add the new Dom element
 333  * @private
 334  */
 335 Cutter.prototype.deserializeObject = function(oSerialized, oParent)
 336 {
 337     var oDom = undefined;
 338     if (oSerialized.nodeType == 1)
 339     {
 340         oDom = document.createElement(oSerialized.tagName);
 341         if (typeof oSerialized.attributes != "undefined")
 342         {
 343             if(typeof jQuery !== "undefined")
 344 			{
 345 				$(oDom).attr(oSerialized.attributes);
 346 			}else
 347 			{
 348 				for(var sKey in oSerialized.attributes)
 349 				{
 350 					if(oSerialized.attributes.hasOwnProperty(sKey))
 351 					{
 352 						oDom.setAttribute(sKey, oSerialized.attributes[sKey]);
 353 					}
 354 				}
 355 			}
 356         }
 357         oParent.appendChild(oDom);
 358     }
 359     else
 360         if (oSerialized.nodeType == 3)
 361         {
 362             if (typeof oSerialized.textContent != "undefined")
 363             {
 364                 oDom = document.createTextNode(oSerialized.textContent);
 365             }
 366             else
 367             {
 368                 if (oSerialized.data)
 369                 {
 370                     oDom = document.createTextNode(oSerialized.data);
 371                 }
 372                 else
 373                 {
 374                     oDom = document.createTextNode(oSerialized.innerText);
 375                 }
 376             }
 377 
 378             oParent.appendChild(oDom);
 379         }
 380     if (typeof oSerialized.childNodes != "undefined")
 381     {
 382         this.loopOnDeserialize(oSerialized.childNodes, oDom);
 383     }
 384 };
 385 /**
 386  * loopOnDeserialize is an utilities method used to loop over all the serialized elements
 387  * @member Cutter.prototype
 388  * @author Tomas Corral Casas
 389  * @version 1.0
 390  * @param  {object} oSerializedElements The JSON object on which to run the loop
 391  * @private
 392  */
 393 Cutter.prototype.loopOnDeserialize = function(oSerializedElements, oParent)
 394 {
 395     for (var sKey in oSerializedElements)
 396     {
 397         if (oSerializedElements.hasOwnProperty(sKey))
 398         {
 399             this.deserializeObject(oSerializedElements[sKey], oParent);
 400         }
 401     }
 402 };
 403 /**
 404  * deserializeSerializedObject is an utilities method used to deserialize all the Dom elements that where serialized and is where the cut is applied.
 405  * This method is the core of the class.
 406  * @member Cutter.prototype
 407  * @author Tomas Corral Casas
 408  * @version 1.0
 409  * @param  {object} oSerialized The JSON object to deserialize
 410  * @param  {object} oParent The DOM object where to put the new finished code.
 411  * @private
 412  */
 413 Cutter.prototype.deserializeSerializedObject = function(oSerialized, oParent)
 414 {
 415     var bLoopOnChilds = false;
 416     if (typeof oSerialized == "undefined")
 417     {
 418         oSerialized = this._getFirstElementOfObject(this.oSerialized);
 419         this.oDocumentFragment = document.createDocumentFragment();
 420         bLoopOnChilds = true;
 421     }
 422     if (typeof oParent == "undefined")
 423     {
 424      	var oLayer = document.createElement("div");
 425 		this.oDocumentFragment.appendChild(oLayer);
 426 	    oParent = oLayer;
 427     }
 428 
 429     this.deserializeObject(oSerialized, oParent);
 430 
 431     if (typeof oSerialized.childNodes != "undefined")
 432     {
 433         this.loopOnDeserialize(oSerialized.childNodes, oParent);
 434     }
 435 };
 436 /**
 437  * serializeDomObject is an utilities method used to serialize all the Dom elements in a JSON object
 438  * This method is the brain of the class.
 439  * @member Cutter.prototype
 440  * @author Tomas Corral Casas
 441  * @version 1.0
 442  * @param  {object} oDom The Dom element to serialize
 443  * @param  {object} oSerializeObject The JSON object where to serialize the Dom element
 444  * @private
 445  */
 446 Cutter.prototype.serializeDomObject = function(oDom, oSerializeObject)
 447 {
 448    	var sId = Math.random() * 15412457562;
 449    	if(this.bTest)
 450    	{
 451 		sId = "__" + (this.nIdTest++) + "__";
 452    	}
 453     if (this.nWordsCounter < this.nWords)
 454     {
 455         var oSerialized = {};
 456         oSerialized.nodeType = oDom.nodeType;
 457         if (typeof oDom.tagName != "undefined")
 458         {
 459             oSerialized.tagName = oDom.tagName.toLowerCase();
 460         }
 461         var aAttributes = oDom.attributes;
 462         if (aAttributes)
 463         {
 464             oSerialized.attributes = {};
 465             var oAttribute = undefined;
 466 
 467             for (var nAttribute = 0, nLenAttributes = aAttributes.length; nAttribute < nLenAttributes; nAttribute++)
 468             {
 469 				oAttribute = aAttributes[nAttribute];
 470 				if(oAttribute.nodeValue)
 471 				{
 472 					oSerialized.attributes[oAttribute.name] = oAttribute.value;
 473 				}
 474             }
 475         }
 476         if (oSerialized.nodeType == 3)
 477         {
 478             var nLastWordsCounter = this.nWordsCounter;
 479             if (typeof oDom.textContent != "undefined")
 480             {
 481                 this.nWordsCounter += this._countWords(this._trim(oDom.textContent));
 482             }
 483             else
 484             {
 485                 if (oDom.data)
 486                 {
 487                     this.nWordsCounter += this._countWords(this._trim(oDom.data));
 488                 }
 489                 else
 490                 {
 491                     this.nWordsCounter += this._countWords(this._trim(oDom.innerText));
 492                 }
 493 
 494             }
 495 
 496 
 497             if (this.nWordsCounter < this.nWords)
 498             {
 499                 if (typeof oDom.textContent != "undefined")
 500                 {
 501                     oSerialized.textContent = oDom.textContent;
 502                 }
 503                 else
 504                 {
 505                     if (oDom.data)
 506                     {
 507                         oSerialized.innerText = oDom.data;
 508                     }
 509                     else
 510                     {
 511                         oSerialized.innerText = oDom.innerText;
 512                     }
 513                 }
 514 
 515             }
 516             else
 517 			{
 518 				this.bNeedViewMore = true;
 519 				if (nLastWordsCounter < this.nWords && this.nWordsCounter > this.nWords)
 520                 {
 521                     if (typeof oDom.textContent != "undefined")
 522                     {
 523                         oSerialized.textContent = this._getOnlyNumberOfWords(oDom.textContent, (this.nWords - nLastWordsCounter));
 524                     }
 525                     else
 526                     {
 527                         if (oDom.data)
 528                         {
 529                             oSerialized.innerText = this._getOnlyNumberOfWords(oDom.data, (this.nWords - nLastWordsCounter));
 530                         }
 531                         else
 532                         {
 533                             oSerialized.innerText = this._getOnlyNumberOfWords(oDom.innerText, (this.nWords - nLastWordsCounter));
 534                         }
 535                     }
 536 
 537                 }
 538                 else
 539                 {
 540 					if (document.body.textContent)
 541                     {
 542                         oSerialized.textContent = "";
 543                     }
 544                     else
 545                     {
 546                         oSerialized.innerText = "";
 547                     }
 548                 }
 549 			}
 550 
 551         }
 552         if (oDom.hasChildNodes())
 553         {
 554             oSerialized.childNodes = {};
 555             for (var nChild = 0, nLenChilds = oDom.childNodes.length; nChild < nLenChilds; nChild++)
 556             {
 557                 this.serializeDomObject(oDom.childNodes[nChild], oSerialized.childNodes);
 558             }
 559         }
 560 
 561         if (typeof oSerializeObject == "undefined")
 562         {
 563             this.oSerialized[sId] = oSerialized;
 564         }
 565         else
 566         {
 567             oSerializeObject[sId] = oSerialized;
 568         }
 569     }
 570 };
 571 /**
 572  * addEvent is the wrapper method to add a new event to the element
 573  * @member Cutter.prototype
 574  * @author Tomas Corral Casas
 575  * @version 1.0
 576  * @private
 577  * @param {DOM} oElement
 578  * @param {String} sType
 579  * @param {Function} fpCallback
 580  */
 581 Cutter.prototype.addEvent = function(oElement, sType, fpCallback)
 582 {
 583 	if(oElement.addEventListener)
 584 	{
 585 		oElement.addEventListener(sType, fpCallback, false);
 586 	}
 587 	else if(oElement.attachEvent)
 588 	{
 589 		oElement.attachEvent("on" + sType, fpCallback);
 590 	}
 591 };
 592 /**
 593  * setBehaviour is the method that applies the behaviour to the "see more" link to get the full content again when makink click on it
 594  * @member Cutter.prototype
 595  * @author Tomas Corral Casas
 596  * @version 1.0
 597  * @private
 598  */
 599 Cutter.prototype.setBehaviour = function()
 600 {
 601     var self = this;
 602     this.addEvent(this.oViewMore, "click", function()
 603     {
 604     	self.showAll();
 605     });
 606 };
 607 /**
 608  * showAll is the method that put the initial content to the target Dom element
 609  * @member Cutter.prototype
 610  * @author Tomas Corral Casas
 611  * @version 1.0
 612  * @private
 613  */
 614 Cutter.prototype.showAll = function()
 615 {
 616     var oTarget = this.oTarget;
 617     var oParent = oTarget.parentNode;
 618     oParent.insertBefore(this.oBackupApplyTo, oTarget);
 619     oParent.removeChild(oTarget);
 620 };
 621 /**
 622  * init is the method that makes all the work.
 623  * Serialize the Dom.
 624  * Deserialize the JSON object to Dom elements only with the words that were wanted
 625  * Remove the firstChild in oDocumentFragment because this node is the oApplyTo content.
 626  * Clean the content on oTarget.
 627  * Create the "see more" link
 628  * Append the "see more" link to the oDocumentFragment.
 629  * Insert the oDocumentFragment content in the oTarget element
 630  * At least the behaviour is applied to the link to make possible to get the original content before the cut.
 631  * @member Cutter.prototype
 632  * @author Tomas Corral Casas
 633  * @version 1.0
 634  */
 635 Cutter.prototype.init = function()
 636 {
 637     this.serializeDomObject(this.oApplyTo);
 638     this.deserializeSerializedObject();
 639 	var oElement = this.oDocumentFragment.childNodes[0];
 640     oElement.removeChild(this.oDocumentFragment.childNodes[0].childNodes[0]);
 641 
 642     this.oTarget.innerHTML = "";
 643     this.createViewMore();
 644 	var oFinalContent = this.oDocumentFragment.childNodes[0].childNodes[this.oDocumentFragment.childNodes[0].childNodes.length-1];
 645 	if(oFinalContent.nodeType !== 3)
 646 	{
 647 		oFinalContent.appendChild(document.createTextNode("..."));
 648 	}
 649 
 650 	if(this.bNeedViewMore && !this.bNotViewMore)
 651 	{
 652 		oElement.appendChild(this.oViewMore);
 653     	this.setBehaviour();
 654 	}
 655 	this.oTarget.appendChild(this.oDocumentFragment);
 656 };
 657 /**
 658  * run is the static method to make API simple.
 659  * @member Cutter
 660  * @author Tomas Corral Casas
 661  * @version 1.0
 662  * @param {DOM} oApplyTo
 663  * @param {DOM} oTarget
 664  * @param {Number} nWords
 665  * @param {Object} oTexts
 666  * @param {Object} oClasses
 667  */
 668 Cutter.run = function(oApplyTo, oTarget, nWords, oTexts, oClasses)
 669 {
 670 	var oCutter = new Cutter();
 671 		oCutter
 672 			.applyTo(oApplyTo)
 673 			.setTarget(oTarget)
 674 			.setWords(nWords);
 675 		if(typeof oTexts !== "undefined")
 676 		{
 677 			oCutter.setTexts(oTexts);
 678 		}
 679 		if(typeof oClasses !== "undefined")
 680 		{
 681 			oCutter.setClasses(oClasses);
 682 		}
 683 		oCutter.init();
 684 }
 685