Multi-line Text Ellipsis

CSS can only add ellipsis to a single line of text. Here’s how to do it in Javascript using lodash:

// Update wrapping text to limit the number of lines.
var maxLines = 2;
var textEls = document.getElementsByClassName('text-els-class');
var textMaxH = Math.ceil(textEls[0].getBoundingClientRect().height) * maxLines;

_.forEach(textEls, function(el) {

    el.style.whiteSpace = 'normal';

    var elH = el.getBoundingClientRect().height;
    var isOverflow = elH > textMaxH;
    var text = el.innerHTML;

    // Remove the last word until the height is within bounds.
    while (elH > textMaxH) {

        text = (text
            .split(' ')
            .slice(0,-1)
            .join(' ')
        );
        el.innerHTML = text;
        elH = el.getBoundingClientRect().height;
    }

    if (isOverflow) {
        title.innerHTML += ' ...';
        title.style.maxHeight = titleMaxH + 'px';
    }
});

Destroying a Javascript API

Problem:

  • A third party JS API doesn’t have a destroy method.
  • The JS API can only be instantiated once per page view.
  • Writing a single page app by its nature requires a dynamic DOM
  • Dynamic DOM will require the JS API to be spun up and destroyed on multiple elements during the life cycle of the page view

What to do?

Answer: Destroy it yourself. By highjacking the events as they are created, you can gather a collection of all events that the JS API has created during instantiation. When it’s time to destroy the JS API, reference your collection of events to remove them from the window:


var _apiE 		= []; // Array of JS API Events.
var _scriptTag 	= null; 

var _methods: {

	initJsApi: function() {
		// Hook the native event listener to capture JS API events.
		// Preserve the original native method.
		var ogAddEventListener = window.addEventListener;

		// Override the native method.
		window.addEventListener = function() {

			// Capture JS API events.
			_apiE.push({
				type: 			arguments[0],
				listener: 		arguments[1],
				useCapture: 	arguments[2]
			});

			// Call the original native method after capturing the event 
			// so JS API and the browser do their business as usual.
			ogAddEventListener.apply(this, arguments);
		};

		// Add the <script> tag to the DOM dynamically to load the JS API.
		// jQuery will inject ?_0000 cache buster to script tag src attribute value.
		// Some sites will not allow the JS API to be requested with the querystring present.
		// Use native element to work around this.
		_scriptTag = document.createElement('script');
		_scriptTag.setAttribute('src', 		'https://js-api-cdn.com/js/third-party-api.min.js');
		_scriptTag.setAttribute('type', 	'text/javascript');
		_scriptTag.setAttribute('id', 		'js-api-script');
		$('html').append(_scriptTag);

		// Wait for the JS API to instantiate after the <script> has been attached to the DOM.
		var intvl = setInterval(function() {

			if (window.jsApi) { // The actual name of the API object would go here.

				clearInterval(intvl);

				// Instantiate the JS API.
				window.jsApi.init({ options });

				// Reset the native event listener to the original method so we stop capturing events.
				window.addEventListener = ogAddEventListener;
			}
		}, 5);
	},

	destroyJsApi: function() {

		// Remove the <script> tag and instance of the JS API.
		$('#js-api-script').remove();
		delete window.jsApi;

		// Remove the events that the JS API added.
		$.each(_apiE, function(i, e) {

			window.removeEventListener(e.type, e.listener, e.useCapture);
		});
	}
};

Danger: other events that are registered while waiting for the JS API to instantiate will also get removed.

WordPress Child Pages

To get the child pages of a WordPress page, update options using query_posts or WP_Query:

$options = ( object ) array(
	'order' 		=> 'DESC',
	'orderby' 		=> 'menu_order',
	'post_parent' 	=> $pg[ 0 ]->ID,
	'post_status' 	=> 'publish',
	'post_type' 	=> 'page',
	'posts_per_page' => 100
);
WP_Query( $options );

Extracting CSS Selectors

Here is a function to get the CSS selector from an element:

var getElementSelector = function($el) {

	var selector 	= $el.get(0).tagName.toLowerCase();
	var id 			= $el.attr('id');
	var cssClass 	= $el.attr('class');

	if (id) 		selector += '#' + id;
	if (cssClass) 	selector += '.' + cssClass.replace(/ /g, '.');

	return selector;
};

If you have the following element in a jQuery object:
<span id="identifire" class="link-wrap">
And this jQuery object was:
$mySpan
Calling the function:
getElementSelector($mySpan)
Would return the string:
span#identifire.link-wrap

Here is another function to get the full CSS path of the element:

var getElementSelectorPath = function($el) {

	var path = getElementSelector($el);

	while($parent = $el.parent()) {

		var selector 	= getElementSelector($parent);

		if (selector.substr(0, 4) == 'html') 	return path;

		path 	= selector + ' > ' + path;
		$el 	= $parent;
	}
};

Note that this function has a dependency on the previous function getElementSelector.

Passing $mySpan to this function:
getElementSelectorPath($mySpan)
Would return a string something like this:
body > div#content-wrap > div#home > p.intro > span#identifire.link-wrap

Assigning DOM Elements to a Plugin

After writing hundreds of jQuery plugins, I found this pattern for assigning DOM elements to a plugin using data attributes. Here goes:

Markup

In the markup, add data attributes that follow this pattern: data-plugin-elem-mypluginname

<div class="carousel-container">
	<div class="carousel-wrap" data-plugin-elem-mypluginname="carouselWrap">
		<div class="carousel" data-plugin-elem-mypluginname="carousel">
			<div class="carouselTile" data-plugin-elem-mypluginname="carouselTile"></div>
			<div class="carouselTile" data-plugin-elem-mypluginname="carouselTile"></div>
			<div class="carouselTile" data-plugin-elem-mypluginname="carouselTile"></div>
			<div class="carouselTile" data-plugin-elem-mypluginname="carouselTile"></div>
			<div class="carouselTile" data-plugin-elem-mypluginname="carouselTile"></div>
			<div class="carouselTile" data-plugin-elem-mypluginname="carouselTile"></div>
		</div>
	</div>

	<div class="controls">
		<a class="prev" href="#todo-prev" data-direction="prev" data-plugin-elem-mypluginname="controlPrev">
			&laquo;Prev
		</a>

		<a class="next" href="#todo-next" data-direction="next" data-plugin-elem-mypluginname="controlNext">
			&raquo;Next
		</a>
	</div>
</div>

Having the assignment in a data attribute accomplishes two things:

  1. The attribute name includes the plugin name so the markup clearly defines the association between the element and the plugin
  2. The classes are left to be used only by CSS as they were intended. If you’re working with a web designer, they can rename and reorganize the CSS classes as they see fit without disabling your plugin’s functionality and throwing Javascript errors.

Javascript Plugin

In the plugin, pull all the elements into one object for easy access:

$.fn.myPluginName = function( options ) {

	var getPluginElements = function() {

		var $pluginEls 	= _$elem.find('[data-plugin-elem-mypluginname]');
		var $els 		= {};

		$.each($pluginEls, function(i, el) {

			var $el 	= $(el);
			var elKey 	= $el.data('plugin-elem-mypluginname');

			if (typeof $els[elKey] == 'object') {
				$els[elKey] 	= $els[elKey].add($el);
			} else {
				$els[elKey] 	= $el;
			}
		});

		return $els;
	};

	var _$elem 	= $(this);
	var _$els 	= getPluginElements();

	// Your code goes here!
};

Then you can access the object properties directly as a jQuery element:

_$els.controlPrev.click();

_$els.carousel.animate(
	{ 'left': -1000 },
	{
		'duration': 1500,
		'easing': 'easeInOutQuint',
		'queue': false
	}
);

$.each(_$els.carouselTile, function(i, el) {
	// Your code goes here!
});

Remove jQuery Plugin Instances

Here’s how to remove all instances of all plugins from an element if the plugin does not include a destroy method:

$.removeData($element.get(0));

This will not remove the bound events. You have to do that separately.
If the plugin you’re trying to remove has namespaced events, you’re in luck:

// Remove namespaced events added using .on()
$element.off('pluginNamespace');
// Remove namespaced events added using .bind()
$element.unbind('.pluginNamespace');

If not, you can examine all of the events attached to an element:

console.log(
	'$element events:', 
	$._data($element.get(0), 'events')
);

Here’s all wrapped up in a handy function:

var destroyCrappyPlugin = function($elem, eventNamespace) {
	var isInstantiated 	= !! $.data($elem.get(0));

	if (isInstantiated) {
		$.removeData($elem.get(0));
		$elem.off(eventNamespace);
		$elem.unbind('.' + eventNamespace);
	}
};