/** Onpage Help. 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 = $('
') .appendTo('body'); help_container.append('') //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 = $('').appendTo(help_container); if(ie_fix) div.append(''); if(_.settings.icon_1) div.append(''); if(_.settings.icon_2) div.append(''); 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 = $(' ').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("").append(''); 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 = ''; var pos1 = result.indexOf(find1); var pos2 = result.indexOf('', 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(/\([\s\S]+?)\<\/pre\>/ig, function(a, b, c){ return ''+c.replace(/\/g , '>')+''; }); } //modify image paths if needed! if(typeof _.settings.img_url === 'function') { excerpt = excerpt.replace(/\') .wrap('') .closest('.panel-heading'); $(this).wrap('
'); $(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 = $('').insertBefore(this); group.attr('id', group_id); var panel_id = 0; group.siblings('.panel').appendTo(group); group.find('.help-panel-toggle') .append('') .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("").append(''); $('.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(''); 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(/\'); }); } catch(e){} } else if(_.settings.code_highlight === 'prism') { try { result = Prism.highlight(result, Prism.languages[language] , language); content.html(result).wrapInner(''); } catch(e){} } }); } };