Inline User Documentation

A freesoftware application I'm currently working on is mainly intended for very non-technical users managing potentially complex stuffs (suppliers, products, variants, orders, bookings, accounting...), but it contains many options and assignable attributes to fullfith many different scenarios which often impact the overall behaviour of the system in unexpected (or hardly-to-be-expected) ways. Some user documentation should be required, but you know: users do not read documentation.

I wanted to display activable and inline help texts in the interface to describe the many buttons and fields available, inform about their most relevant and interesting side-effects, and eventually provide some hint about their usage (so to introduce new users to most advanced usage patterns). One of my requirements was that such documentation has to be wrote by another non-technical user, the one who provides me most of guidance and feedback about the effective ways and logics and methods that application should apply, and the only one who probably knows the application even better than me.

After some research and experiment, I ended in a handy (apparent) solution: write documentation in Markdown, and dispose it on the DOM of the web interface adopting some simple rule.

This solution leverages Bootstrap (especially for conventions about the HTML tags hierarchy on forms, and built-in popovers), jQuery, and marked (an highly customizable parser for Markdown).

Following, a proof-of-concept.

Javascript file:


function helpFillNode(nodes, text) {
	if (nodes != null) {
		/*
			Here, a special class is added to highlight elements with documentation
			attached
		*/
		nodes.parent().addClass('help-sensitive').popover({
			content: text,
			placement: 'auto right',
			container: 'body',
			html: true,
			trigger: 'hover'
		});
	}

	return '';
}

function setupHelp() {
	/*
		#help-trigger is intended to be the switch to enable/disable inline help
	*/
	$('body').on('click', '#help-trigger', function(e) {
		e.preventDefault();

		if ($(this).hasClass('active')) {
			$('.help-sensitive').removeClass('help-sensitive').popover('destroy');
		}
		else {
			/*
				Here we get the external file containing the documentation, in
				Markdown format
			*/
			$.get('data.md', function(data) {
				var renderer = new marked.Renderer();
				var container = null;
				var nodes = null;
				var inner_text = '';
				
				/*
					We overwrite the rendering functions to not render
					anything, but dispose parsed contents on the DOM
				*/

				renderer.heading = function (text, level) {
					inner_text = helpFillNode(nodes, inner_text);

					if (level == 2)
						container = $(text);
					else if (level == 1)
						nodes = container.find(':contains(' + text + ')').last();
				};
				renderer.paragraph = function (text, level) {
					if (inner_text != '')
						inner_text += '<br/>';
					inner_text += text;
				};
				renderer.list = function (text, level) {
					inner_text += '<ul>' + text + '</ul>';
				};

				/*
					In the end, the rendering function will trigger the above
					functions accordly to the contents of the Markdown document
				*/
				marked(data, {renderer: renderer}, function() {
					/*
						Note that the function used to attach a message
						to a node is invoked also in the end, to flush the
						latest accumulated "inner_text"
					*/
					inner_text = helpFillNode(nodes, inner_text);
				});
			});
		}

		$(this).toggleClass('active');
		return false;
	});
}

HTML file:


<form class="form-horizontal my-target-form">
	<div class="row">
		<div class="col-md-6">
			<div class="form-group">
				<label for="first" class="col-sm-3 control-label">First Label</label>
				<div class="col-sm-9">
					<input type="text" class="form-control" name="first">
				</div>
			</div>
			
			<div class="form-group">
				<label for="second" class="col-sm-3 control-label">Second Label</label>
				<div class="col-sm-9">
					<input type="text" class="form-control" name="second">
				</div>
			</div>
		</div>
	</div>
</form>

Markdown file:


## .my-target-form

# First Label

Fill me with a random text.

Or eventually leave blank.

# Second Label

Here you can:

  * write random stuff
  * write a random set of characters
  * leave blank

When the Markdown file is parsed:

  • second level titles are used to match a container in the page using a jQuery selector
  • first level titles are the text of <label> or <button> you want to document
  • the text below is the attached message. It can be even split on multiple paragraphs or lists

In the end, each message is assigned to the node matching $('second_level_title :contains(first_level_title)').parent()

Video of the current implementation: