/**
|
<b>Onpage Help</b>. You can use this to provide help dialogs on your application pages. See docs for more info.
|
*/
|
|
window.Onpage_Help = function(options) {
|
var $ = window.jQuery || null;
|
if($ == null) return;
|
|
options = options || {}
|
var defaults = {
|
include_all: true,
|
icon_1: 'fa fa-question',
|
icon_2: 'fa fa-lightbulb-o',
|
base: '',
|
code_highlight: (!!window.Rainbow ? 'rainbow' : (!!window.Prism ? 'prism' : null)),
|
|
add_panels: true,
|
panel_content_selector: '.info-section',
|
panel_content_title: '.info-title'
|
}
|
this.settings = $.extend({}, defaults, options);
|
|
|
var $base = this.settings['base'];
|
var ie_fix = document.all && !window.atob;//ie9 and below need a little fix
|
|
var section_start = {};
|
var section_end = {};
|
var section_rect = {};
|
var section_count = 0;
|
|
var created = false;
|
var active = false;
|
|
var self = this, _ = this;
|
var ovfx = '';
|
var help_container = null;
|
|
var body_h, body_w;
|
|
var captureFocus = function() {
|
if(!help_container) return;
|
var scroll = -1;
|
//like bootstrap modal
|
$(document)
|
.off('focusin.ace.help') //remove any previously attached handler
|
.on('focusin.ace.help', function (e) {
|
if (!( help_container[0] == e.target || $.contains(help_container[0], e.target) )) {
|
help_container.focus();
|
}
|
|
if(e.target == document && scroll > -1) {
|
//when window regains focus and container is focused, it scrolls to bottom
|
//so we put it back to its place
|
$('body,html').scrollTop(scroll);
|
scroll = -1;
|
}
|
})
|
|
$(window).on('blur.ace.help', function(){
|
scroll = $(window).scrollTop();
|
});
|
}
|
var releaseFocus = function() {
|
$(document).off('focusin.ace.help');
|
$(window).off('blur.ace.help');
|
}
|
|
|
this.toggle = function() {
|
if(active) {
|
self.disable();
|
}
|
else {
|
self.enable();
|
}
|
}
|
|
this.enable = function() {
|
if(active) return;
|
if(typeof _.settings.before_enable === 'function' && _.settings.before_enable.call(self) === false) return;
|
////
|
|
//if( !created ) this.init();
|
active = true;
|
|
$('.onpage-help-backdrop, .onpage-help-section').removeClass('hidden');
|
|
ovfx = document.body.style.overflowX;
|
document.body.style.overflowX = 'hidden';//hide body:overflow-x
|
|
display_help_sections();
|
captureFocus();
|
|
////
|
if(typeof _.settings.after_enable === 'function') _.settings.after_enable.call(self);
|
}
|
|
this.disable = function() {
|
if(!active) return;
|
if(typeof _.settings.before_disable === 'function' && _.settings.before_disable.call(self)) return;
|
////
|
|
active = false;
|
$('.onpage-help-backdrop, .onpage-help-section').addClass('hidden');
|
|
document.body.style.overflowX = ovfx;//restore body:overflow-x
|
releaseFocus();
|
|
////
|
if(typeof _.settings.after_disable === 'function') _.settings.after_disable.call(self);
|
}
|
|
this.is_active = function() {
|
return active;
|
}
|
this.show_section_help = function(section) {
|
launch_help_modal(section, true);
|
}
|
|
|
this.init = function() {
|
if( created ) return;
|
|
help_container =
|
$('<div class="onpage-help-container" id="onpage-help-container" tabindex="-1" />')
|
.appendTo('body');
|
|
help_container.append('<div class="onpage-help-backdrop hidden" />')
|
|
//update to correct position and size
|
$(window).on('resize.onpage_help', function() {
|
if(!active) return;
|
display_help_sections();
|
|
if( help_modal != null && help_modal.hasClass('in') ) {
|
setBodyHeight();
|
disableBodyScroll();
|
}
|
})
|
|
created = true;
|
}
|
this.init();//create once at first
|
|
|
///////////////////////////
|
this.update_sections = function() {
|
save_sections(true);//reset sections, maybe because of new elements and comments inserted into DOM
|
}
|
|
|
function display_help_sections() {
|
if(!active) return;
|
|
save_sections();//finds comments and relevant help sections
|
|
body_h = document.body.scrollHeight - 2;
|
body_w = document.body.scrollWidth - 2;
|
|
//we first calculate all positions
|
//because if we calculate one position and then make changes to DOM,
|
//next position calculation will become slow on Webkit, because it tries to re-calculate layout changes and things
|
//i.e. we batch call all and save offsets and scrollWidth, etc and then use them later in highlight_section
|
//Firefox doesn't have such issue
|
for(var name in section_start) {
|
if(section_start.hasOwnProperty(name)) {
|
save_section_offset(name);
|
}
|
}
|
for(var name in section_start) {
|
if(section_start.hasOwnProperty(name)) {
|
highlight_section(name);
|
}
|
}
|
}
|
|
|
//finds comments and relevant help sections
|
function save_sections(reset) {
|
if( !(reset === true || section_count == 0) ) return;//no need to re-calculate sections, then return
|
if(reset === true) help_container.find('.onpage-help-section').remove();
|
|
section_start = {};
|
section_end = {};
|
section_count = 0;
|
|
var count1 = 0, count2 = 0;
|
|
//find all relevant comments
|
var comments = $('*').contents().filter(function(){ return this.nodeType == 8/**Node.COMMENT_NODE;*/ })
|
$(comments).each(function() {
|
var match
|
if( (match = $.trim(this.data).match(/#section\s*:\s*([\w\d\-\.\/]+)/i)) ) {
|
var section_name = match[1];
|
if( !(section_name in section_start) ) section_start[ section_name ] = this;
|
}
|
if( (match = $.trim(this.data).match(/\/section\s*:\s*([\w\d\-\.\/]+)/i)) ) {
|
var section_name = match[1];
|
if( !(section_name in section_end) && (section_name in section_start) ) {
|
section_end[ section_name ] = this;
|
section_count++;
|
}
|
}
|
})
|
}
|
|
|
|
function save_section_offset(name) {
|
if( !(name in section_start) || !(name in section_end) ) return;
|
|
var x1 = 1000000, y1 = 1000000, x2 = -1000000, y2 = -1000000;
|
var visible = false;
|
|
|
var elements = [];
|
|
var start = section_start[name];
|
var end = section_end[name];
|
while(start != end) {
|
start = start.nextSibling;
|
if(start == null) break;
|
else if(start.nodeType == 1 /**Node.ELEMENT_NODE*/) elements.push(start);
|
}
|
|
var elen = elements.length;
|
if(elen > 0 && !_.settings['include_all']) {
|
//calculate dimension of only first and last element
|
elements = elen == 1 ? [elements[0]] : [elements[0], elements[elen - 1]]
|
}
|
|
$(elements).each(function() {
|
var $this = $(this);
|
if( $this.is(':hidden') ) return;
|
|
var off = $this.offset();
|
var w = $this.outerWidth();
|
var h = $this.outerHeight();
|
|
if( !off || !w || !h ) return;
|
|
visible = true;
|
if(off.left < x1) x1 = off.left;
|
if(off.left + w > x2) x2 = off.left + w;
|
|
if(off.top < y1) y1 = off.top;
|
if(off.top + h > y2) y2 = off.top + h;
|
});
|
|
|
if( !visible ) {
|
section_rect[name] = {is_hidden: true}
|
return;
|
}
|
|
x1 -= 1;
|
y1 -= 1;
|
x2 += 1;
|
y2 += 1;
|
|
|
var width = x2 - x1, height = y2 - y1;
|
//section_rect is out of window ???
|
if(x1 + width < 2 || x1 > body_w || y1 + height < 2 || y1 > body_h ) {
|
section_rect[name] = {is_hidden: true}
|
return;
|
}
|
|
section_rect[name] = {
|
'left': parseInt(x1),
|
'top': parseInt(y1),
|
'width': parseInt(width),
|
'height': parseInt(height)
|
}
|
}
|
|
|
function highlight_section(name) {
|
if( !(name in section_rect) || !help_container ) return;
|
|
//div is the highlighted box above each section
|
var div = help_container.find('.onpage-help-section[data-section="'+name+'"]').eq(0);
|
if(div.length == 0) {
|
div = $('<a class="onpage-help-section" href="#" />').appendTo(help_container);
|
if(ie_fix) div.append('<span class="ie-hover-fix" />');
|
|
if(_.settings.icon_1) div.append('<i class="help-icon-1 '+_.settings.icon_1+'"></i>');
|
if(_.settings.icon_2) div.append('<i class="help-icon-2 '+_.settings.icon_2+'"></i>');
|
|
div.attr('data-section', name);
|
|
div.on('click', function(e) {
|
e.preventDefault();
|
launch_help_modal(name);
|
});
|
}
|
|
var rect = section_rect[name];
|
if(rect['is_hidden'] === true) {
|
div.addClass('hidden');
|
return;
|
}
|
|
div.css({
|
left: rect.left,
|
top: rect.top,
|
width: rect.width,
|
height: rect.height
|
});
|
|
|
div.removeClass('hidden');
|
div.removeClass('help-section-small help-section-smaller');
|
if(rect.height < 55 || rect.width < 55) {
|
div.addClass('help-section-smaller');
|
}
|
else if(rect.height < 75 || rect.width < 75) {
|
div.addClass('help-section-small');
|
}
|
}
|
|
|
var nav_list = [];
|
var nav_pos = -1;
|
var mbody = null;
|
var maxh = 0;
|
var help_modal = null;
|
|
//disable body scroll, when modal content has no scrollbars or reached end of scrolling
|
function disableBodyScroll() {
|
if (!mbody) return;
|
|
var body = mbody[0];
|
var disableScroll = body.scrollHeight <= body.clientHeight;
|
|
//mousewheel library available?
|
var mousewheel_event = !!$.event.special.mousewheel ? 'mousewheel.ace.help' : 'mousewheel.ace.help DOMMouseScroll.ace.help';
|
|
mbody.parent()
|
.off(mousewheel_event)
|
.on(mousewheel_event, function(event) {
|
if(disableScroll) event.preventDefault();
|
else {
|
event.deltaY = event.deltaY || 0;
|
var delta = (event.deltaY > 0 || event.originalEvent.detail < 0 || event.originalEvent.wheelDelta > 0) ? 1 : -1
|
|
if(delta == -1 && body.scrollTop + body.clientHeight >= body.scrollHeight) event.preventDefault();
|
else if(delta == 1 && body.scrollTop <= 0) event.preventDefault();
|
}
|
});
|
}
|
|
function setBodyHeight() {
|
if (!mbody) return;
|
|
var diff = parseInt(help_modal.find('.modal-dialog').css('margin-top'));
|
diff = diff + 110 + parseInt(diff / 2);
|
maxh = parseInt( $(window).innerHeight() - diff + 40 );
|
mbody.css({'max-height': maxh});
|
}
|
|
|
function launch_help_modal(section_name, save_to_list) {
|
if(help_modal == null) {
|
help_modal = $('<div id="onpage-help-modal" class="modal onpage-help-modal" tabindex="-1" role="dialog" aria-labelledby="HelpModalDialog" aria-hidden="true">\
|
<div class="modal-dialog modal-lg">\
|
<div class="modal-content">\
|
<div class="modal-header">\
|
<div class="pull-right onpage-help-modal-buttons">\
|
<button aria-hidden="true" data-navdir="up" type="button" class="disabled btn btn-white btn-success btn-sm"><i class="ace-icon fa fa-level-up fa-flip-horizontal bigger-125 icon-only"></i></button>\
|
\
|
<button aria-hidden="true" data-navdir="back" type="button" class="disabled btn btn-white btn-info btn-sm"><i class="ace-icon fa fa-arrow-left icon-only"></i></button>\
|
<button aria-hidden="true" data-navdir="forward" type="button" class="disabled btn btn-white btn-info btn-sm"><i class="ace-icon fa fa-arrow-right icon-only"></i></button>\
|
\
|
<button aria-hidden="true" data-dismiss="modal" class="btn btn-white btn-danger btn-sm" type="button"><i class="ace-icon fa fa-times icon-only"></i></button>\
|
</div>\
|
<h4 class="modal-title">Help Dialog</h4>\
|
</div>\
|
<div class="modal-body"><div class="onpage-help-content"></div></div>\
|
</div>\
|
</div>\
|
</div>').appendTo('body');
|
|
mbody = help_modal.find('.modal-body');
|
mbody.css({'overflow-y': 'auto', 'overflow-x': 'hidden'});
|
|
help_modal.css({'overflow' : 'hidden'})
|
.on('show.bs.modal', function() {
|
releaseFocus();
|
})
|
.on('hidden.bs.modal', function() {
|
captureFocus();
|
})
|
|
help_modal.find('.onpage-help-modal-buttons').on('click', 'button[data-navdir]', function() {
|
var dir = $(this).attr('data-navdir');
|
if(dir == 'back') {
|
if(nav_pos > 0) {
|
nav_pos--;
|
launch_help_modal(nav_list[nav_pos], false);
|
}
|
}
|
else if(dir == 'forward') {
|
if(nav_pos < nav_list.length - 1) {
|
nav_pos++;
|
launch_help_modal(nav_list[nav_pos], false);//don't save to history list, already in the list
|
}
|
}
|
else if(dir == 'up') {
|
var $this = $(this), url;
|
if( $this.hasClass('disabled') || !(url = $this.attr('data-url')) ) return;
|
|
launch_help_modal(url , true);//add to history list
|
}
|
});
|
}
|
|
|
if( !help_modal.hasClass('in') ) {
|
if( document.body.lastChild != help_modal[0] ) $(document.body).append(help_modal);//move it to become the last child of body
|
help_modal.modal('show');
|
|
setBodyHeight();
|
}
|
|
help_modal.find('.modal-title').wrapInner("<span class='hidden' />").append('<i class="fa fa-spinner fa-spin blue bigger-125"></i>');
|
var content = $('.onpage-help-content');
|
content.addClass('hidden')
|
|
$(document.body).removeClass('modal-open');//modal by default hides body scrollbars, but we don't want to do so, because on modal hide, a winow resize is triggered
|
|
var parts = section_name.match(/file\:(.*?)\:(.+)/i);
|
if(parts && parts.length == 3) {
|
display_codeview(parts[2], parts[1], false);
|
return;
|
}
|
|
section_name = section_name.replace(/^#/g, '');
|
if(typeof _.settings.section_url === 'function') url = _.settings.section_url.call(self, section_name);
|
|
$.ajax({url: url, dataType: 'text'})
|
.done(function(result) {
|
//find the title for this dialog by looking for a tag that has data-id attribute
|
var title = '', excerpt = '';
|
|
if(typeof _.settings.section_title === 'function') title = _.settings.section_title.call(self, result, section_name, url);
|
else {
|
var escapeSpecialChars = function(name) {
|
return name.replace(/[\-\.\(\)\=\"\'\\\/]/g, function(a,b){return "\\"+a;})
|
}
|
var tname = section_name;
|
while(title.length == 0) {
|
var reg_str = '\\<([a-z][a-z0-9]*)(?:\\s+)(?:[^\\<\\>]+?)data\\-id\\=\\"\\#'+escapeSpecialChars(tname)+'\\"(?:[^\\>]*)\\>([\\s\\S]*?)</\\1>';
|
|
var regexp = new RegExp(reg_str , "im");
|
var arr = result.match(reg_str);
|
if(arr && arr[2]) {
|
title = arr[2];
|
break;
|
}
|
|
//if no "#something.part" was not found try looking for "#something" instead
|
var tpos
|
if((tpos = tname.lastIndexOf('.')) > -1) {
|
tname = tname.substr(0, tpos);
|
} else break;
|
}
|
}
|
|
help_modal.find('.modal-title').html( $.trim(title) || ' ' );
|
|
if(typeof _.settings.section_content === 'function') excerpt = _.settings.section_content.call(self, result, section_name, url);
|
else {
|
var find1 = '<!-- #section:'+section_name+' -->';
|
var pos1 = result.indexOf(find1);
|
var pos2 = result.indexOf('<!-- /section:'+section_name+' -->', pos1);
|
|
if(pos1 == -1 || pos2 == -1) {
|
help_modal.find('.modal-title').html( ' ' );
|
return;
|
}
|
|
excerpt = result.substring(pos1 + find1.length + 1, pos2);
|
}
|
|
|
//convert `<` and `>` to `<` and `>` inside code snippets
|
if(typeof _.settings.code_highlight === 'function') {
|
excerpt = _.settings.code_highlight.call(self, excerpt);
|
}
|
else {
|
//find prism & rainbow style pre tags and replace < > characters with < >
|
excerpt =
|
excerpt.replace(/\<pre((?:(?:.*?)(?:data\-language=["'](?:[\w\d]+)["'])(?:.*?))|(?:(?:.*?)(?:class=["'](?:.*?)language\-(?:[\w\d]+)(?:.*?)["'])(?:.*?)))\>([\s\S]+?)\<\/pre\>/ig, function(a, b, c){
|
return '<pre'+(b)+'>'+c.replace(/\</g , '<').replace(/\>/g , '>')+'</pre>';
|
});
|
}
|
|
|
//modify image paths if needed!
|
if(typeof _.settings.img_url === 'function') {
|
excerpt = excerpt.replace(/\<img(?:(?:.*?)src=["']([^"']+)["'])/ig, function(img, src) {
|
var new_src = _.settings.img_url.call(self, src);
|
return img.replace(src, new_src)
|
});
|
}
|
|
|
//now update content area
|
content.empty().append(excerpt);
|
if(typeof _.settings.code_highlight === 'function') {
|
_.settings.code_highlight.call(self, content);
|
}
|
else if(_.settings.code_highlight === 'rainbow') {
|
try {
|
Rainbow.color(content[0]);
|
} catch(e) {}
|
}
|
else if(_.settings.code_highlight === 'prism') {
|
try {
|
content.find('pre[class*="language-"],code[class*="language-"]').each(function() {
|
Prism.highlightElement(this);
|
})
|
} catch(e) {}
|
}
|
|
|
//wrap titles and contents inside panels
|
if(_.settings.add_panels) {
|
content
|
.find(_.settings.panel_content_selector).each(function() {
|
var header = $(this).prevAll(_.settings.panel_content_title);
|
if(header.length == 0) return false;
|
|
header =
|
header.attr('class', 'panel-title')
|
.wrapInner('<a class="help-panel-toggle" href="#" data-parent="#" data-toggle="collapse" />')
|
.wrap('<div class="panel-heading" />')
|
.closest('.panel-heading');
|
|
$(this).wrap('<div class="panel panel-default panel-help"><div class="panel-collapse collapse"><div class="panel-body"></div></div></div>');
|
$(this).closest('.panel').prepend(header);
|
})
|
|
var group_count = $('.panel-group').length;
|
content.find('.panel').each(function() {
|
if( $(this).parent().hasClass('panel-group') ) return;
|
|
var group_id = 'panel-group-help-'+ (++group_count);
|
var group = $('<div class="panel-group" />').insertBefore(this);
|
group.attr('id', group_id);
|
|
var panel_id = 0;
|
group.siblings('.panel').appendTo(group);
|
group.find('.help-panel-toggle')
|
.append('<i class="pull-right ace-icon fa fa-plus" data-icon-show="ace-icon fa fa-plus" data-icon-hide="ace-icon fa fa-minus"></i>')
|
.attr('data-parent', '#'+group_id)
|
.each(function() {
|
panel_id++;
|
$(this).attr('data-target', '#'+group_id+'-'+panel_id);
|
$(this).closest('.panel-heading').siblings('.panel-collapse').attr('id', group_id+'-'+panel_id);
|
});
|
});
|
$(document).off('click.help-panel-toggle', '.help-panel-toggle').on('click.help-panel-toggle', '.help-panel-toggle', function(e) {
|
e.preventDefault();
|
});
|
}
|
|
|
///////////////////////////////////////////
|
|
content.removeClass('hidden')
|
|
var images = content.find('img:visible');
|
if(images.length > 0) {
|
//handle scrollbars when all images are loaded
|
var ev_count = 0;
|
images.off('.help_body_scroll').on('load.help_body_scroll error.help_body_scroll', function() {
|
$(this).off('.help_body_scroll');
|
ev_count++;
|
if(ev_count >= images.length) disableBodyScroll();
|
});
|
}
|
|
disableBodyScroll();
|
content.find('.panel > .panel-collapse').on('shown.bs.collapse hidden.bs.collapse', function() {
|
disableBodyScroll();
|
});
|
|
|
//save history list
|
add_to_nav_list(section_name, save_to_list);
|
|
var pos = -1;
|
if((pos = section_name.lastIndexOf('.')) > -1) {
|
section_name = section_name.substr(0, pos);
|
help_modal.find('button[data-navdir=up]').removeClass('disabled').attr('data-url', section_name);
|
}
|
else {
|
help_modal.find('button[data-navdir=up]').addClass('disabled').removeAttr('data-url').blur();
|
}
|
})
|
.fail(function() {
|
help_modal.find('.modal-title').find('.fa-spin').remove().end().find('.hidden').children().unwrap();
|
});
|
}//launch_help_modal
|
|
|
$(document).on('click', '.onpage-help-modal a[href^="http"]', function() {
|
$(this).attr('target', '_blank');
|
});
|
|
$(document).on('click', '.help-more', function(e) {
|
e.preventDefault();
|
var href = $(this).attr('href');
|
launch_help_modal(href);
|
});
|
|
|
|
function add_to_nav_list(section_name, save_to_list) {
|
if(save_to_list !== false) {
|
if(nav_list.length > 0) {
|
nav_list = nav_list.slice(0, nav_pos + 1);
|
}
|
if(nav_list[nav_list.length - 1] != section_name) {
|
nav_list.push(section_name);
|
nav_pos = nav_list.length - 1;
|
}
|
}
|
|
if(nav_pos == 0){
|
help_modal.find('button[data-navdir=back]').addClass('disabled').blur();
|
}
|
else {
|
help_modal.find('button[data-navdir=back]').removeClass('disabled');
|
}
|
|
if(nav_pos == nav_list.length - 1){
|
help_modal.find('button[data-navdir=forward]').addClass('disabled').blur();
|
}
|
else {
|
help_modal.find('button[data-navdir=forward]').removeClass('disabled');
|
}
|
}
|
|
|
$(document).on('click', '.open-file[data-open-file]', function() {
|
help_modal.find('.modal-title').wrapInner("<span class='hidden' />").append('<i class="fa fa-spinner fa-spin blue bigger-125"></i>');
|
$('.onpage-help-content').addClass('hidden')
|
|
var url = $(this).attr('data-path') || $(this).text();
|
var language = $(this).attr('data-open-file');
|
display_codeview(url, language, true);
|
});
|
|
|
function display_codeview(url, language, save_to_list) {
|
var $url = url;
|
|
if(typeof _.settings.file_url === 'function') url = _.settings.file_url.call(self, url, language);
|
$.ajax({url: url, dataType:'text'})
|
.done(function(result) {
|
|
add_to_nav_list('file:'+language+':'+$url, save_to_list);
|
|
help_modal.find('button[data-navdir=up]').addClass('disabled').blur();
|
help_modal.find('.modal-title').html($url).wrapInner('<code />');
|
|
if(language != 'json') {
|
if(language != 'css') {
|
//replace each tab character with two spaces (only those that start at a new line)
|
result = result.replace(/\n[\t]{1,}/g, function(p, q) {
|
return p.replace(/\t/g, " ");
|
});
|
} else {
|
result = result.replace(/\t/g , " ")
|
}
|
}
|
else {
|
language = 'javascript';
|
result = JSON.stringify(JSON.parse(result), null, 2);//add spacing and somehow beautification
|
}
|
|
result = result.replace(/\>/g, '>').replace(/\</g, '<');
|
|
var content = $('.onpage-help-content');
|
content.removeClass('hidden').empty();
|
|
if(typeof _.settings.code_highlight === 'function') {
|
result = _.settings.code_highlight.call(self, result, language);
|
content.html(result);
|
}
|
else if(_.settings.code_highlight === 'rainbow') {
|
try {
|
Rainbow.color(result, language, function(highlighted_code) {
|
content.html(highlighted_code).wrapInner('<pre data-language="'+language+'" />');
|
});
|
} catch(e){}
|
}
|
else if(_.settings.code_highlight === 'prism') {
|
try {
|
result = Prism.highlight(result, Prism.languages[language] , language);
|
content.html(result).wrapInner('<pre class="language-'+language+'" />');
|
} catch(e){}
|
}
|
|
});
|
}
|
|
};
|