diff --git a/lib/jsduck/app.rb b/lib/jsduck/app.rb index c5ee691757a918b16b2a2c194c3f6566f8a9bb8b..47c4c9bffbec4bc03cc9951a5aa6e94398464c4b 100644 --- a/lib/jsduck/app.rb +++ b/lib/jsduck/app.rb @@ -245,6 +245,7 @@ module JsDuck out_dir = out_path + "/" + guide_name puts "Creating guide #{out_dir} ..." if @verbose FileUtils.cp_r(in_dir, out_dir) + formatter.doc_context = {:filename => out_dir + "/README.md", :linenr => 0} guide = formatter.format(IO.read(out_dir + "/README.md")) guide.gsub!(/ guide}) diff --git a/lib/jsduck/doc_formatter.rb b/lib/jsduck/doc_formatter.rb index d4100cf8922e2d989c73e32266647de56817d64e..460a5604909a01ef16bcefcf1dea2b630248f0c3 100644 --- a/lib/jsduck/doc_formatter.rb +++ b/lib/jsduck/doc_formatter.rb @@ -31,7 +31,11 @@ module JsDuck # Sets up instance to work in context of particular class, so # that when {@link #blah} is encountered it knows that # Context#blah is meant. - attr_accessor :context + attr_accessor :class_context + + # Sets up instance to work in context of particular doc object. + # Used for error reporting. + attr_accessor :doc_context # Maximum length for text that doesn't get shortened, defaults to 120 attr_accessor :max_length @@ -44,7 +48,8 @@ module JsDuck attr_accessor :relations def initialize - @context = "" + @class_context = "" + @doc_context = {} @max_length = 120 @relations = {} @link_tpl = '%a' @@ -86,7 +91,7 @@ module JsDuck target = $1 text = $2 if target =~ /^(.*)#(.*)$/ - cls = $1.empty? ? @context : $1 + cls = $1.empty? ? @class_context : $1 member = $2 else cls = target @@ -97,12 +102,22 @@ module JsDuck if text text = text elsif member - text = (cls == @context) ? member : (cls + "." + member) + text = (cls == @class_context) ? member : (cls + "." + member) else text = cls end - link(cls, member, text) + file = @doc_context[:filename] + line = @doc_context[:linenr] + if !@relations[cls] + puts "Warning: #{file} line #{line} #{input} links to non-existing class." + input + elsif member && !get_member_type(cls, member) + puts "Warning: #{file} line #{line} #{input} links to non-existing member." + input + else + link(cls, member, text) + end end end diff --git a/lib/jsduck/exporter.rb b/lib/jsduck/exporter.rb index 7e6a3866e6df71f5e6217884d6ead023b1a6c9e9..b6c9d2ebdcb53bc61e31be7335e0649d0ca99af1 100644 --- a/lib/jsduck/exporter.rb +++ b/lib/jsduck/exporter.rb @@ -35,7 +35,8 @@ module JsDuck # converts :doc properties from markdown to html and resolve @links def format_class(c) - @formatter.context = c[:name] + @formatter.class_context = c[:name] + @formatter.doc_context = c c[:doc] = @formatter.format(c[:doc]) if c[:doc] [:cfg, :property, :method, :event, :cssVar, :cssMixin].each do |type| c[type] = c[type].map {|m| format_member(m) } @@ -45,6 +46,7 @@ module JsDuck def format_member(m) m = m.clone + @formatter.doc_context = m m[:doc] = @formatter.format(m[:doc]) if m[:doc] if m[:params] || @formatter.too_long?(m[:doc]) m[:shortDoc] = @formatter.shorten(m[:doc]) diff --git a/lib/jsduck/page.rb b/lib/jsduck/page.rb index e8b2ce898e0f42c099155c2f16f16f6c64396094..ed077fddbd10b8d4c79bb427e4d12637a3292b47 100644 --- a/lib/jsduck/page.rb +++ b/lib/jsduck/page.rb @@ -21,7 +21,7 @@ module JsDuck @relations = relations @cache = cache @formatter = DocFormatter.new - @formatter.context = cls.full_name + @formatter.class_context = cls.full_name @formatter.link_tpl = '%a' @formatter.relations = relations end diff --git a/spec/doc_formatter_spec.rb b/spec/doc_formatter_spec.rb index 7e40650d560a470fe9e1cec47a774382b9096aec..3387bb8049031fb4a86f02a0f7b8a8b067e2ea8d 100644 --- a/spec/doc_formatter_spec.rb +++ b/spec/doc_formatter_spec.rb @@ -4,7 +4,7 @@ describe JsDuck::DocFormatter do before do @formatter = JsDuck::DocFormatter.new - @formatter.context = "Context" + @formatter.class_context = "Context" @formatter.relations = { 'Context' => JsDuck::Class.new({ :method => [{:name => "bar", :tagname => :method}] diff --git a/template/index.html b/template/index.html index 74c8498659178e15481c1f818e56c08b7ab7a7e1..2efb8f69f7c33016a5d1106a19b95640d4c3d4dc 100644 --- a/template/index.html +++ b/template/index.html @@ -24,6 +24,7 @@ + diff --git a/template/js/HoverMenuButton.js b/template/js/HoverMenuButton.js new file mode 100644 index 0000000000000000000000000000000000000000..aa1c88d98c5cce537eec3b4f451893eb26539098 --- /dev/null +++ b/template/js/HoverMenuButton.js @@ -0,0 +1,120 @@ +/** + * Toolbar button with menu that appears when hovered over. + */ +Ext.define('Docs.HoverMenuButton', { + extend: 'Ext.toolbar.TextItem', + componentCls: "hover-menu-button", + + /** + * @cfg {[String]} links + * Array of HTML anchor elements to be shown in menu. + */ + links: [], + + statics: { + // Global list of all menus. + // So we can hide all other menus while showing a specific one. + menus: [] + }, + + initComponent: function() { + this.addEvents( + /** + * @event click + * Fired when button clicked. + */ + "click" + ); + + // Append links count to button text + this.text += ' ' + this.links.length + ''; + + this.callParent(arguments); + }, + + onRender: function() { + this.callParent(arguments); + + this.renderMenu(); + + this.getEl().on({ + click: function() { + this.fireEvent("click"); + }, + mouseover: function() { + // hide other menus + Ext.Array.forEach(Docs.HoverMenuButton.menus, function(menu) { + if (menu !== this.menu) { + menu.setStyle({display: "none"}); + } + }); + // stop pending menuhide process + clearTimeout(this.hideTimeout); + // position menu right below button and show it + var p = this.getEl().getXY(); + this.menu.setStyle({ + left: p[0]+"px", + top: (p[1]+23)+"px", + display: "block" + }); + }, + mouseout: this.deferHideMenu, + scope: this + }); + + this.menu.on({ + mouseover: function() { + clearTimeout(this.hideTimeout); + }, + mouseout: this.deferHideMenu, + scope: this + }); + }, + + onDestroy: function() { + // clean up DOM + this.menu.remove(); + // remove from global menu list + Ext.Array.remove(Docs.HoverMenuButton.menus, this.menu); + + this.callParent(arguments); + }, + + renderMenu: function() { + this.menu = Ext.get(Ext.core.DomHelper.append(document.body, { + html: this.renderMenuHtml(), + cls: 'hover-menu-menu' + })); + Docs.HoverMenuButton.menus.push(this.menu); + }, + + renderMenuHtml: function() { + // divide links into columns with at most 25 links in one column + var columns = []; + for (var i=0; i', + '', + '', + '', + '', + '{.}', + '', + '', + '', + '', + '' + ); + return tpl.apply({columns: columns}); + }, + + deferHideMenu: function() { + this.hideTimeout = Ext.Function.defer(function() { + this.menu.setStyle({display: "none"}); + }, 200, this); + } + +}); diff --git a/template/js/OverviewToolbar.js b/template/js/OverviewToolbar.js index 5c49cf98882a2509777d301f7028da603f11d2d8..6b4a2dfb60d5616e558ed5760b6a2751648d3bdf 100644 --- a/template/js/OverviewToolbar.js +++ b/template/js/OverviewToolbar.js @@ -26,18 +26,15 @@ Ext.define('Docs.OverviewToolbar', { var members = this.docClass[type]; if (members.length) { this.items.push(this.createMemberButton({ - items: members, + text: memberTitles[type], type: type, - title: memberTitles[type] + members: members })); } } if (this.docClass.subclasses.length) { - this.items.push(this.createSubClassesButton({ - items: this.docClass.subclasses, - title: "Sub Classes" - })); + this.items.push(this.createSubClassesButton(this.docClass.subclasses)); } this.items = this.items.concat([ @@ -76,53 +73,41 @@ Ext.define('Docs.OverviewToolbar', { }, createMemberButton: function(cfg) { - var menu = Ext.create('Ext.menu.Menu', { - items: Ext.Array.map(cfg.items, function(m) { - return { - text: m.name, - memberName: cfg.type + '-' + m.name - }; - }), - plain: true, - listeners: { - click: function(menu, item) { - Ext.getCmp('doc-overview').scrollToEl("#" + item.memberName); - } - } - }); - - return Ext.create('Ext.button.Split', { - cls: cfg.type, - iconCls: 'icon-' + cfg.type, - text: cfg.title + ' ' + cfg.items.length + '', + return Ext.create('Docs.HoverMenuButton', { + text: cfg.text, + cls: 'icon-'+cfg.type, + links: Ext.Array.map(cfg.members, function(m) { + return this.createLink(this.docClass.name, m); + }, this), listeners: { click: function() { Ext.getCmp('doc-overview').scrollToEl("#m-" + cfg.type); } - }, - menu: menu + } }); }, - createSubClassesButton: function(cfg) { - var menu = Ext.create('Ext.menu.Menu', { - items: Ext.Array.map(cfg.items, function(className) { - return {text: className, clsName: className}; - }), - plain: true, - listeners: { - click: function(menu, item) { - Docs.ClassLoader.load(item.clsName); - } - } + createSubClassesButton: function(subclasses) { + return Ext.create('Docs.HoverMenuButton', { + text: "Sub Classes", + cls: 'icon-subclass', + links: Ext.Array.map(subclasses, function(cls) { + return this.createLink(cls); + }, this) }); + }, - return Ext.create('Ext.button.Button', { - cls: 'subcls', - iconCls: 'icon-subclass', - text: cfg.title + ' ' + cfg.items.length + '', - menu: menu - }); + // Creates HTML link to class (and optionally to class member) + createLink: function(cls, member) { + if (member) { + var url = cls+"-"+member.tagname+"-"+member.name; + var label = member.name; + } + else { + var url = cls; + var label = cls; + } + return Ext.String.format('{1}', url, label); }, hideInherited: function(el) { diff --git a/template/resources/sass/style.scss b/template/resources/sass/style.scss index 89b043c858b151652b4c2f8c5c149c8dfed802a7..11c6aa59af580678d06b3eaae3d9fb8ea0a69b5b 100644 --- a/template/resources/sass/style.scss +++ b/template/resources/sass/style.scss @@ -753,33 +753,38 @@ pre, code, kbd, samp, tt { .expandAllMembers { background: url(../images/expandcollapse.png) no-repeat -14px 2px; } .collapseAllMembers { - background: url(../images/expandcollapse.png) no-repeat 2px 2px; } - .member_sm { - position: absolute; - padding: 5px 15px 10px; - background: #eaeaea; - z-index: 8; - top: 21px; - line-height: 1.3em; - border: 1px solid #bfbfbf; - border-top: 1px solid #eaeaea; - left: -16px; - border-radius-bottom: 5px; - h2 { - font-weight: bold; - text-decoration: underline; - padding-bottom: 5px; } - a { - display: block; - position: relative; - padding: 2px 30px 2px 0; - color: #0464bb; - white-space: nowrap; - &:hover { - color: #083772; - text-decoration: underline; } } - .l { - vertical-align: top; } } } + background: url(../images/expandcollapse.png) no-repeat 2px 2px; } } +.hover-menu-button { + padding-left: 20px; + cursor: pointer; } +.hover-menu-menu { + display: none; // hide initially + font-size: 12px; + position: absolute; + padding: 5px 15px 10px; + background: #eaeaea; + z-index: 8; + top: 21px; + line-height: 1.3em; + border: 1px solid #bfbfbf; + border-top: 1px solid #eaeaea; + left: -16px; + border-radius-bottom: 5px; + h2 { + font-weight: bold; + text-decoration: underline; + padding-bottom: 5px; } + a { + display: block; + position: relative; + padding: 2px 30px 2px 0; + color: #0464bb; + white-space: nowrap; + &:hover { + color: #083772; + text-decoration: underline; } } + td { + vertical-align: top; } } #doc-source { .x-panel-body-default {