// load the css stylesheet
ajs.css('mooreadall.css');
// look at initialize method for class description
ajs.tools.mooreadall = (function() {
// private members
var _private = {};
return new Class({
Implements: [Options],
// options: global options
options: {
words: 60,
remove_tags: [], // 'all' | array with tag elements
display_style: 'block', // block | inline | <all display allowed properties> | visible (visibility instead of display) | none
truncate_characters: '...',
action_label: 'read all',
action: 'layer', // inplace' | 'layer' | 'link' | 'callback'
return_label: 'back', // inplace type
layer_id: '', // need to customize every layer?
layer_width: 800,
layer_draggable: false,
layer_resizable: false,
layer_text_resizable: false,
link_href: '',
link_target: '_blank',
callback: null,
callback_param: null
},
/**
* @summary Tool designed to cut html string preserving the html structure, without breaking tags.
* @classdesc <p>Provides a "read all" link which may call other pages, callback functions, open a layer or show the whole content in the same element</p>
*
* @constructs ajs.tools.mooreadall
* @param {Object} [options] A class options object
* @param {Number} [options.words=60] The number of words after which the text is cut off.
* @param {String|Array} [options.remove_tags=new Array()] Tag elements to be removed. Possible values are:
* <ul>
* <li>"all": all tags are removed</li>
* <li>["tag1", "tag2"]: only the listed tags are removed</li>
* </ul>
* @param {String} [options.display_style='block'] I don't like at all to see a whole content reduce itself after a while, I prefer to see a cut content growing up from an empty element.<br />
* So mooReadAll hide the element whose content is cut and show it again when computation finishes. The best would be to hide directly the
* element and have mooReadAll to show it. With this option is possible to decide which style property to use:
* the display or the visibility one. Possible values are:
* <ul>
* <li>"none": the hide/show actions are skipped</li>
* <li>"visible": the visibility style is used ('hidden' and 'visible')</li>
* <li>"every display style": the element is hidden setting "display: none" and the shown setting display to the value
* passed ('block', 'inline', 'table', 'list-item', ...)</li>
* @param {String} [options.truncate_characters='...'] Characters displayed after text truncation
* @param {String} [options.action_label='read all'] Text of the link which displays the whole content
* @param {String} [options.action='layer'] The action to perform when clicking the action_label link. Possible values are:
* <ul>
* <li>"inplace": the whole content is rendered inside the same element, a back button appears so that the content may be expanded/compressed
* infinite times</li>
* <li>"link": the action_label links to another page (an anchor tag)</li>
* <li>"callback": a callback function to call when clicking the action_label link. The first parameter passed to callback is the element,
* then a custom parameter and finally the context of the mooreadall object is passed also</li>
* <li>"layer": the whole content is displayed in a layer over the document (lightbox style). It's possible to activate some controls:
* drag, resize, text-resize</li>
* </ul>
* @param {String} [options.return_label='back'] Used by the action "inplace" type. Is the link label which appears after expanding the whole content in order to compress it again
* @param {String} [options.layer_id=''] Used by the action "layer" type. if you need to customize every layer, this id is assigned to the id attribute of the layer.
* @param {Number} [options.layer_width=800] Used by the action "layer" type: The width of the layer. Its height may be set by css.
* @param {Number} [options.layer_draggable=fase] Used by the action "layer" type. Whether to make the layer draggable or not.
* @param {Number} [options.layer_resizable=fase] Used by the action "layer" type. Whether to make the layer resizable or not.
* @param {Number} [options.layer_text_resizable=fase] Used by the action "layer" type. Whether to make the layer text resizable or not.
* @param {String} [options.link_href=fase] Used by the action "link" type. The url to link the action_label to.
* @param {String} [options.link_target='_blank'] Used by the action "link" type. The target attribute of the anchor tag.
* @param {Function} [options.callback=null] Used by the action "callback" type. The callback function to call when clicking the action_label link.
* @param {Mixed} [options.callback_param=null] Used by the action "callback" type. A custom parameter to pass to the callback function.
* @example
* var mr = new ajs.tools.mooreadall({
* action: 'inplace'
* });
* mr.add('.expand');
*
*/
initialize: function(options) {
this.id = String.uniqueID();
_private[this.id] = {
max_z_index: ajs.shared.getMaxZindex(),
max_text_size: 22,
min_text_size: 8
};
if(options) this.setOptions(options);
},
/**
* @summary Creates the object used to apply the tool, merging global options and local options (passed to the add or apply methods)
* @memberof ajs.tools.mooreadall.prototype
* @method
* @protected
* @return {Object} The final options object
*/
setProperties: function(opts) {
var prop = {
words: typeof opts.words != 'undefined'
? opts.words.toInt()
: this.options.words.toInt(),
remove_tags: typeOf(opts.remove_tags) === 'array' || (typeOf(opts.remove_tags) === 'string' && opts.remove_tags === 'all')
? opts.remove_tags
: this.options.remove_tags,
truncate_characters: typeOf(opts.truncate_characters) === 'string'
? opts.truncate_characters
: this.options.truncate_characters,
display_style: typeOf(opts.display_style) === 'string'
? opts.display_style
: this.options.display_style,
action: typeOf(opts.action) === 'string'
? opts.action
: this.options.action,
action_label: typeOf(opts.action_label) === 'string'
? opts.action_label
: this.options.action_label,
return_label: typeOf(opts.return_label) === 'string'
? opts.return_label
: this.options.return_label,
link_href: typeOf(opts.link_href) === 'string'
? opts.link_href
: this.options.link_href,
link_target: typeOf(opts.link_target) === 'string'
? opts.link_target
: this.options.link_target,
callback: typeOf(opts.callback) === 'function'
? opts.callback
: this.options.callback,
callback_param: typeof opts.callback_param != 'undefined'
? opts.callback_param
: this.options.callback_param,
layer_id: typeOf(opts.layer_id) === 'string'
? opts.layer_id
: this.options.layer_id,
layer_width: typeof opts.layer_width != 'undefined'
? opts.layer_width.toInt()
: this.options.layer_width.toInt(),
layer_draggable: typeOf(opts.layer_draggable) === 'boolean'
? opts.layer_draggable
: this.options.layer_draggable,
layer_resizable: typeOf(opts.layer_resizable) === 'boolean'
? opts.layer_resizable
: this.options.layer_resizable,
layer_text_resizable: typeOf(opts.layer_text_resizable) === 'boolean'
? opts.layer_text_resizable
: this.options.layer_text_resizable
}
return prop;
}.protect(),
/**
* @summary Applies the text truncation to the given elements with the given options
* @memberof ajs.tools.mooreadall.prototype
* @param {Mixed} elements The elements whose content has to be truncated. Possible values are:
* <ul>
* <li>A css selector</li>
* <li>An array of dom elements</li>
* <li>A dom element</li>
* </ul>
* @param {Object} opts The options to use when performing the cut action, see the constructor for the available options
* @return void
*
*/
add: function(elements, opts) {
if(typeOf(elements)==='string') _private[this.id].elements = $$(elements);
else if(typeOf(elements)==='elements') _private[this.id].elements = elements;
else if(typeOf(elements)==='element') _private[this.id].elements = [elements];
else _private[this.id].elements = [];
for(var i = 0; i < _private[this.id].elements.length; i++) {
this.apply(_private[this.id].elements[i], opts);
}
},
/**
* @summary Applies the text truncation to the given element with the given options
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element
* @param {Object} opts The options to use when performing the cut action
* @return void
*
*/
apply: function(element, opts) {
// local options may override global ones
if(typeOf(opts) !== 'object') opts = {};
var prop = this.setProperties(opts);
// store the full html text
var html = element.get('html');
// hide elementing while computing (better if element is already hidden)
this.hideElement(element, prop);
// new cut text and "read all" action link
var cut_html = this.cut(html, prop);
var action_link = this.actionLink(element, html, cut_html, prop);
element.set('html', cut_html);
action_link.inject(element, 'bottom');
// now that computing finishes show the element
this.showElement(element, prop);
},
/**
* @summary Hides the element which contains the text to truncate basing upon the display_style option
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element
* @param {Object} prop The definitive option object
* @method
* @protected
* @return void
*
*/
hideElement: function(element, prop) {
if(prop.display_style=='none') return 0;
else if(prop.display_style=='visible') element.style.visibility = 'hidden';
else element.style.display = 'none';
}.protect(),
/**
* @summary Shows the element which contains the text to truncate basing upon the display_style option
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element
* @param {Object} prop The definitive option object
* @method
* @protected
* @return void
*
*/
showElement: function(element, prop) {
if(prop.display_style=='none') return 0;
if(prop.display_style=='visible') element.style.visibility = prop.display_style;
else element.style.display = prop.display_style;
}.protect(),
/**
* @summary Performs the cut of the html content preserving the html structure and good formatting
* @memberof ajs.tools.mooreadall.prototype
* @param {String} html The whole html content to cut. Global or local options are used (number of words, remove_tags)
* @param {Object} prop The definitive option object
* @return {String} The truncated html text
*
*/
cut: function(html, prop) {
// open close tags like <br /> are changed by javascript innerHTML function to <br>
var rexp_open_tag = "<([a-zA-Z][a-zA-Z123456]*)\s?[^>]*?>";
var rexp_close_tag = "<\/([a-zA-Z][a-zA-Z123456]*)>";
var rexp_tag = "<\/?[a-zA-Z][a-zA-Z123456]*\s?[^>]*?\/?>";
var ot_regexp = new RegExp(rexp_open_tag);
var ct_regexp = new RegExp(rexp_close_tag);
// if text words are less than prop.words return html
var replace_regexp = new RegExp(rexp_open_tag+"|"+rexp_close_tag, "g");
var text = html.replace(replace_regexp, "");
var text_array = text.split(" ");
if(text_array.length <= prop.words) return html;
// else cut!
// get all <opentag|closetag|openclosetag>text till other < divided in matches
var parse_regexp = new RegExp("("+rexp_tag+")?[^<]*", "g");
var matches = html.match(parse_regexp);
var cut_html = '';
var opened_tag = [];
var counter = 0;
for(var i=0; i<matches.length; i++) {
if(matches[i].trim()) {
var part = matches[i];
// separate the tag part and the text part
var part_regexp = new RegExp("("+rexp_tag+")?([^<]*)", "g");
part_matches = part_regexp.exec(part);
if(typeof part_matches[1] != 'undefined') {
var tag = part_matches[1];
// is an open tag?
if(tag_match = tag.match(ot_regexp)) {
tag_name = tag_match[1];
// if tag is to be removed
if(prop.remove_tags.contains(tag_name) || prop.remove_tags==='all') {
// do nothing
}
// img and br are open close tag, but detected as open tags
else if(tag_name == "img" || tag_name == "br") {
// close tag and add it to the partial text
cut_html += tag.substr(0, tag.length-1)+" />";
}
else {
opened_tag.push(tag_name);
// add tag to the partial text
cut_html += tag;
}
}
else if(tag_match = tag.match(ct_regexp)) {
tag_name = tag_match[1];
if(prop.remove_tags.contains(tag_name) || prop.remove_tags === 'all') {
// do nothing
}
else {
// last opened tag is closed, remove it from list
opened_tag.pop();
// add tag to the partial text
cut_html += tag;
}
}
}
// if there is some text
if(typeof part_matches[2] != 'undefined') {
var text_array = part_matches[2].split(" ");
// if the first element is empty that's because the text begins with a space, so
// we have to add it manually if the text overcomes words length
var add_space = /^\s*$/.test(text_array[0]) ? true : false;
text_array.erase("");
// sometimes at the beginning of the content there are tabs or multiple spaces due to indentation
// if not sufficient may be necessary to erase all text_array elements
// which doesn't contain no-space characters, clearly only for the words count
if(/^\s*$/.test(text_array[0])) text_array.splice(0,1);
var text_add_array = [];
// if text does not overcome words length
if(counter + text_array.length < prop.words) {
cut_html += part_matches[2];
counter += text_array.length;
}
// else add only some words
else {
var diff = prop.words - counter;
for(var i=0; i<diff; i++) {
text_add_array.push(text_array[i]);
}
if(add_space) cut_html += " ";
cut_html += text_add_array.join(" ");
// max length reached then break
break;
}
}
}
}
// add truncate characters
cut_html += prop.truncate_characters;
// close opened tags
for(var i=opened_tag.length-1; i>=0; i--) {
cut_html += "</"+opened_tag[i]+">";
}
// if all tags have been removed add a space to separate the action_label link
if(prop.remove_tags==='all') cut_html += " ";
return cut_html;
},
/**
* @summary Returns the anchor/span "read all" link, depending on the action option.
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element which contains the text to truncate
* @param {String} html The whole content
* @param {String} cut_html The truncated content
* @param {Object} prop The definitive option object
* @return {String} The action controller
*
*/
actionLink: function(element, html, cut_html, prop) {
var tag = prop.action === 'link' ? 'a' : 'span';
// store label
var action_label = prop.action_label;
var action_link = new Element(tag, {
'class': 'link',
'html': action_label
});
if(prop.action === 'inplace') {
// store return_label in a variable which doesn't change with the object
var return_label = prop.return_label;
action_link.addEvent('click', function() {
element.set('html', html);
if(return_label) {
var return_link = new Element('span', {'class': 'link', 'html': return_label});
return_link.addEvent('click', function() {
element.set('html', cut_html);
action_link.set('html', action_label);
action_link.inject(element, 'bottom');
}.bind(this));
return_link.inject(element, 'bottom');
}
}.bind(this))
}
else if(prop.action === 'link') {
action_link.setProperty('href', prop.link_href);
action_link.setProperty('target', prop.link_target);
}
else if(prop.action === 'callback') {
action_link.addEvent('click', prop.callback.bind(this, element, prop.callback_param));
}
else if(prop.action === 'layer') {
action_link.addEvent('click', this.showInLayer.bind(this, element, html, prop));
}
return action_link;
},
/**
* @summary Shows the entire html text in a layer (lightbox style).
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element which contains the text to truncate
* @param {String} html The whole content
* @param {Object} prop The definitive option object
* @return void
*
*/
showInLayer: function(element, html, prop) {
// overlay doesn't like active objects
this.disableObjects();
// render overlay and after (chain) the layer
this.renderOverlay(element, html, prop);
},
/**
* @summary Renders the layer.
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element which contains the text to truncate
* @param {String} html The whole content
* @param {Object} prop The definitive option object
* @return void
*/
renderLayer: function(element, html, prop) {
// init layer
_private[this.id].layer = new Element('div', {'class': 'mra_layer', id: prop.layer_id});
_private[this.id].layer.setStyles({
position: 'absolute',
width: prop.layer_width,
visibility: 'hidden',
'z-index': ++_private[this.id].max_z_index
});
// layer title and body
this.layerTitle(element);
this.layerBody(html);
// layer rendering
_private[this.id].layer.inject(document.body);
var coord = _private[this.id].layer.getCoordinates();
_private[this.id].layer.setStyles({
top: ajs.shared.getViewport().cY-coord.height/2,
left: ajs.shared.getViewport().cX-coord.width/2,
visibility: 'visible'
});
// extra options
if(prop.layer_resizable) this.makeResizable();
if(prop.layer_text_resizable) this.makeTextResizable();
if(prop.layer_draggable) this.makeDraggable();
// close button always present
var ico_close = new Element('div', {'class': 'mra_close'});
ico_close.inject(_private[this.id].layer, 'top');
ico_close.addEvent('click', function() { this.closeLayer(); }.bind(this));
},
/**
* @summary Creates the layer title taking the data-title element attribute.
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element which contains the text to truncate
* @return void
* @method
* @protected
*/
layerTitle: function(element) {
// I love html5 ;)
if(typeof element.getProperty('data-title') != 'undefined') {
var title = new Element('div', {'class': 'mra_title'});
title.set('html', element.getProperty('data-title'));
title.inject(_private[this.id].layer, 'top');
}
}.protect(),
/**
* @summary Sets the layer content.
* @memberof ajs.tools.mooreadall.prototype
* @param {String} html The html content
* @return void
* @method
* @protected
*/
layerBody: function(html) {
var layer_body = new Element('div', {'class': 'mra_layer_body'});
layer_body.set('html', html);
layer_body.inject(_private[this.id].layer);
}.protect(),
/**
* @summary Closes the layer.
* @memberof ajs.tools.mooreadall.prototype
* @return void
*/
closeLayer: function() {
_private[this.id].layer.destroy();
_private[this.id].overlay_anim.start(0.7, 0).chain(function() { _private[this.id].overlay.dispose(); }.bind(this));
this.enableObjects();
},
/**
* @summary Makes the layer draggable.
* @memberof ajs.tools.mooreadall.prototype
* @return void
* @method
* @protected
*/
makeDraggable: function() {
var ico_drag = new Element('div', {'class': 'mra_drag'});
ico_drag.inject(_private[this.id].layer, 'top');
var docDim = document.getCoordinates();
var dragInstance = new Drag(_private[this.id].layer, {
'handle':ico_drag,
'limit':{'x':[0, (docDim.width-_private[this.id].layer.getCoordinates().width)], 'y':[0, ]}
});
}.protect(),
/**
* @summary Makes the layer resizable.
* @memberof ajs.tools.mooreadall.prototype
* @return void
* @method
* @protected
*/
makeResizable: function() {
var ico_resize = new Element('div', {'class': 'mra_resize'});
ico_resize.inject(_private[this.id].layer, 'bottom');
var ylimit = $$('body')[0].getSize().y-20;
var xlimit = $$('body')[0].getSize().x-20;
_private[this.id].layer.makeResizable({
'handle': ico_resize,
'limit': {'x':[200, xlimit], 'y':[60, ylimit]}
});
}.protect(),
/**
* @summary Makes the layer text resizable.
* @memberof ajs.tools.mooreadall.prototype
* @return void
* @method
* @protected
*/
makeTextResizable: function() {
var ico_text_smaller = new Element('div', {'class': 'mra_text_smaller'});
ico_text_smaller.addEvent('click', function() {
new_size = _private[this.id].layer.getStyle('font-size').toInt() < (_private[this.id].min_text_size+1)
? _private[this.id].min_text_size
: _private[this.id].layer.getStyle('font-size').toInt() - 1;
_private[this.id].layer.setStyle('font-size', new_size+'px');
}.bind(this));
ico_text_smaller.inject(_private[this.id].layer, 'top');
var ico_text_bigger = new Element('div', {'class': 'mra_text_bigger'});
ico_text_bigger.addEvent('click', function() {
new_size = _private[this.id].layer.getStyle('font-size').toInt() > (_private[this.id].max_text_size-1)
? _private[this.id].max_text_size
: _private[this.id].layer.getStyle('font-size').toInt() + 1;
_private[this.id].layer.setStyle('font-size', new_size+'px');
}.bind(this));
ico_text_bigger.inject(_private[this.id].layer, 'top');
}.protect(),
/**
* @summary Checks if a window object is of the same domain as the main one.
* @memberof ajs.tools.mooreadall.prototype
* @param win {Element} The window object
* @return {Boolean} Whether or not the given window has the same domain
* @method
* @protected
*/
sameDomain: function(win) {
var H = location.href;
local = H.substring(0, H.indexOf(location.pathname));
try {
win = win.document;
return win && win.URL && win.URL.indexOf(local) == 0;
}
catch(e) {
return false;
}
}.protect(),
/**
* @summary Disables document objects.
* @memberof ajs.tools.mooreadall.prototype
* @return void
* @method
* @protected
*/
disableObjects: function() {
for(var i=0;i<window.frames.length;i++) {
var myFrame = window.frames[i];
if(this.sameDomain(myFrame)) {
var obs = myFrame.document.getElementsByTagName('object');
for(var ii=0; ii<obs.length; ii++) {
obs[ii].style.visibility='hidden';
}
}
}
$$('object').each(function(item) {
item.style.visibility='hidden';
})
}.protect(),
/**
* @summary Enables document objects.
* @memberof ajs.tools.mooreadall.prototype
* @return void
* @method
* @protected
*/
enableObjects: function() {
for(var i=0;i<window.frames.length;i++) {
var myFrame = window.frames[i];
if(this.sameDomain(myFrame)) {
var obs = myFrame.document.getElementsByTagName('object');
for(var ii=0; ii<obs.length; ii++) {
obs[ii].style.visibility='visible';
}
}
}
$$('object').each(function(item) {
item.style.visibility='visible';
})
}.protect(),
/**
* @summary Renders an overlay (lightbox style).
* @memberof ajs.tools.mooreadall.prototype
* @param {Element} element The dom element which contains the text to truncate
* @param {String} html The whole content
* @param {Object} prop The definitive option object
* @return void
* @method
* @protected
*/
renderOverlay: function(element, html, prop) {
var docDim = document.getScrollSize();
_private[this.id].overlay = new Element('div', {'class': 'mra_overlay'});
_private[this.id].overlay.setStyles({
position: 'absolute',
top: '0px',
left: '0px',
width: docDim.x,
height: docDim.y,
'z-index': ++_private[this.id].max_z_index,
opacity: 0
});
_private[this.id].overlay.inject(document.body);
_private[this.id].overlay_anim = new Fx.Tween(_private[this.id].overlay, {property: 'opacity'});
_private[this.id].overlay_anim.start(0, 0.7).chain(function() { this.renderLayer(element, html, prop); }.bind(this));
}.protect()
});
}());