/* @author: SHIMODA Hiroshi aka Piro @url: http://piro.sakura.ne.jp/ @title: Outline @description: Provides outline tree of webpages. @version: 0.3.1 @license: MIT */ var STYLE = ol { margin: 1em !important; padding-left: 2em !important; } #-moz-outline-jetpack-list { display: none; /* hide by default */ } #-moz-outline-jetpack-list[in-animation] > ol { visibility: hidden; } body#-moz-outline-jetpack-list { display: block !important; position: static !important; -moz-border-radius: 0 !important; -moz-box-shadow: none !important; } ]]>.toString(); var OutlineService = { MAX_LABEL_LENGTH : 20, showInContent : true, initDocument : function(aDocument) { if (!this.showInContent) return; var list = this.createOutline(aDocument); if (!list || list.getElementsByTagName('li').length < 2) return; this.addStyle(aDocument); var body = aDocument.getElementsByTagName('body')[0]; var showList = function(aEvent) { aEvent.preventDefault(); aEvent.stopPropagation(); div.setAttribute('in-animation', true); div.style.visibility = 'hidden'; div.style.display = 'block'; var width = div.offsetWidth + 16; var height = div.offsetHeight + 24; div.style.width = div.style.height = '32px'; div.style.visibility = 'visible'; tab.style.display = 'none'; $(div).animate( { width: width, height: height }, 'fast', function() { div.removeAttribute('in-animation'); } ); div.addEventListener('dblclick', hideList, false); aDocument.addEventListener('click', hideList, false); }; var hideList = function(aEvent) { if ( aEvent.button != 0 || ( aEvent.type == 'click' && OutlineService.isEventFiredInList(aEvent) ) ) return; aEvent.preventDefault(); aEvent.stopPropagation(); div.removeEventListener('dblclick', hideList, false); aDocument.removeEventListener('click', hideList, false); div.setAttribute('in-animation', true); $(div).animate( { width: 32, height: 32 }, 'fast', function() { tab.style.display = 'block'; div.style.display = 'none'; div.style.width = div.style.height = ''; div.removeAttribute('in-animation'); } ); }; var div = aDocument.createElement('div'); div.setAttribute('id', '-moz-outline-jetpack-list'); div.setAttribute('class', '-moz-outline-jetpack-item'); div.appendChild(list); body.appendChild(div); var tab = aDocument.createElement('div'); tab.setAttribute('id', '-moz-outline-jetpack-list-tab'); tab.setAttribute('class', '-moz-outline-jetpack-item'); tab.setAttribute('active', true); tab.addEventListener('click', showList, false); body.appendChild(tab); }, createOutline : function(aDocument) { var outline = createOutline(aDocument); return outline.length ? this.createList(outline, aDocument) : null ; }, createList : function(aSections, aDocument) { var list = aDocument.createElement('ol'); aSections.forEach(function(aSection) { list.appendChild(this.craeteItem(aSection, aDocument)); }, this); return list; }, craeteItem : function(aSection, aDocument) { var item = aDocument.createElement('li'); if (aSection.headElement) { var link = aDocument.createElement('a'); link.setAttribute('href', '#'+aSection.headElement.id); link.addEventListener('click', function(aEvent) { if (aEvent.button != 0) return; aEvent.preventDefault(); aEvent.stopPropagation(); $('html, body', aDocument).animate( { scrollTop: aSection.headElement.offsetTop }, 'normal' ); }, false); var label = aSection.headElement.textContent; link.setAttribute('title', label); label = this.crop(aSection.headElement.textContent, this.MAX_LABEL_LENGTH); link.appendChild(aDocument.createTextNode(label)); item.appendChild(link); } if (aSection.childs.length) item.appendChild(this.createList(aSection.childs, aDocument)); return item; }, crop : function(aString, aMax) { return aString.length > aMax ? aString.slice(0, aMax)+'...' : aString ; }, isEventFiredInList : function(aEvent) { return aEvent.view.document.evaluate( 'ancestor-or-self::*[@class="-moz-outline-jetpack-item"]', aEvent.target, null, Components.interfaces.nsIDOMXPathResult.BOOLEAN_TYPE, null ).booleanValue }, addStyle : function(aDocument) { var style = aDocument.createElement('style'); style.setAttribute('type', 'text/css'); style.appendChild(aDocument.createTextNode(STYLE)); aDocument.getElementsByTagName('head')[0].appendChild(style); } }; // =================================================================== // outline parser made by amachang // http://d.hatena.ne.jp/amachang/20090915/1252999677 function createOutline(document) { // Let current outlinee be null. (It holds the element whose outline is being created.) var outlinee = null; // Let current section be null. (It holds a pointer to a section, so that elements in the DOM can all be associated with a section.) var section = null; // Create a stack to hold elements, which is used to handle nesting. Initialize this stack to empty. var stack = []; var nextSectionId = 1; // As you walk over the DOM in tree order, trigger the first relevant step below for each element as you enter and exit it. (function (element) { var elementInfo = { element: element, outline: null }; if (trigger(elementInfo, true)) { return true; } var child = element.firstChild; if (child) { do { if (child.nodeType == 1) { if (arguments.callee(child)) { return true; } } } while (child = child.nextSibling); } if (trigger(elementInfo, false)) { return true; } return false; })(document.documentElement); function trigger(elementInfo, enter) { var exit = !enter; var goNext = false; // If the top of the stack is an element, and you are exiting that element if (exit && stack[stack.length - 1] == elementInfo) { // Pop that element from the stack. stack.pop(); } // If the top of the stack is a heading content element else if (stack.length && isHeading(stack[stack.length - 1].element)) { // Do nothing. } // When entering a sectioning content element or a sectioning root element else if (enter && (isSectioningContent(elementInfo.element) || isSectioningRoot(elementInfo.element))) { // If current outlinee is not null, push current outlinee onto the stack. if (outlinee) { stack.push(outlinee); } // Let current outlinee be the element that is being entered. outlinee = elementInfo; // Let current section be a newly created section for the current outlinee element. section = { nested: [], element: elementInfo, id: nextSectionId++ }; // Let there be a new outline for the new current outlinee, outlinee.outline = []; // initialized with just the new current section as the only section in the outline. outlinee.outline.push(section); outlinee.outline.lastSection = section; } // When exiting a sectioning content element, if the stack is not empty else if (exit && isSectioningContent(elementInfo.element) && stack.length) { // Pop the top element from the stack, and let the current outlinee be that element. outlinee = stack.pop(); // Let current section be the last section in the outline of the current outlinee element. section = outlinee.outline.lastSection; // Append the outline of the sectioning content element being exited to the current section. // (This does not change which section is the last section in the outline.) for (var i = 0; i < elementInfo.outline.length; i ++) { section.nested.push(elementInfo.outline[i]); } } // When exiting a sectioning root element, if the stack is not empty else if (exit && isSectioningRoot(elementInfo.element) && stack.length) { // Pop the top element from the stack, and let the current outlinee be that element. outlinee = stack.pop(); // Let current section be the last section in the outline of the current outlinee element. section = outlinee.outline.lastSection; // Finding the deepest child: If current section has no child sections, stop these steps. while (section.nested.length) { // Let current section be the last child section of the current current section. section = section.nested[section.nested.length - 1]; // Go back to the substep labeled finding the deepest child. } } // When exiting a sectioning content element or a sectioning root element else if (exit && (isSectioningContent(elementInfo.element) || isSectioningRoot(elementInfo.element))) { // Let current section be the first section in the outline of the current outlinee element. section = outlinee.outline[0]; // Skip to the next step in the overall set of steps. (The walk is over.) goNext = true; } // If the current outlinee is null. else if (!outlinee) { // Do nothing. } // When entering a heading content element else if (enter && isHeading(elementInfo.element)) { // If the current section has no heading, if (!section.head) { // let the element being entered be the heading for the current section. section.head = elementInfo; } // Otherwise, if the element being entered has a rank equal to or greater than // the heading of the last section of the outline of the current outlinee, else if (getRank(elementInfo.element) >= getRank(outlinee.outline.lastSection.head.element)) { // then create a new section var newSection = { nested: [], element: elementInfo, id: nextSectionId++ }; // and append it to the outline of the current outlinee element, // so that this new section is the new last section of that outline. outlinee.outline.push(newSection); outlinee.outline.lastSection = newSection; // Let current section be that new section. section = newSection; // Let the element being entered be the new heading for the current section. section.head = elementInfo; } // Otherwise, run these substeps: else { // Let candidate section be current section. var candidateSection = section; // (step 2) while (true) { // If the element being entered has a rank lower than the rank of the heading of the candidate section, if (getRank(elementInfo.element) < getRank(candidateSection.head.element)) { // then create a new section, var newSection = { nested: [], element: elementInfo, id: nextSectionId++ }; // and append it to candidate section. // (This does not change which section is the last section in the outline.) candidateSection.nested.push(newSection); // Let current section be this new section. section = newSection; // Let the element being entered be the new heading for the current section. section.head = elementInfo; // Abort these substeps. break; } // Let new candidate section be the section that contains candidate section in the outline of current outlinee. var newCandidateSection = (function (parent, list) { for (var i = 0; i < list.length; i ++) { var section = list[i]; if (section == candidateSection) { return parent; } else { var r = arguments.callee(section, section.nested); if (r) { return r; } } } return null; })(null, outlinee.outline); // Let candidate section be new candidate section. candidateSection = newCandidateSection; // Return to step 2. } } // Push the element being entered onto the stack. // (This causes the algorithm to skip any descendants of the element.) stack.push(elementInfo); } else { // Do nothing. } // In addition, whenever you exit a node, after doing the steps above, // if current section is not null, if (exit && section) { // associate the node with the section current section. // Note: not imlementation } return goNext; } // If the current outlinee is null, // then there was no sectioning content element or sectioning root element in the DOM. There is no outline. Abort these steps. if (!outlinee) { return null; } // Associate any nodes that were not associated with a section in the steps above with current outlinee as their section. // Associate all nodes with the heading of the section with which they are associated, if any. // Note: not imlementation // If current outlinee is the body element, then the outline created for that element is the outline of the entire document. if (outlinee.element == document.body) { return (function (list) { var result = []; for (var i = 0; i < list.length; i ++) { var section = list[i]; var obj = { childs: arguments.callee(section.nested) }; obj.headElement = section.head ? section.head.element : null; obj.element = section.element ? section.element.element : null; result.push(obj); } return result; })(outlinee.outline); } return null; function getRank(element) { switch (element.tagName.toLowerCase()) { case 'h1': return 6; case 'h2': return 5; case 'h3': return 4; case 'h4': return 3; case 'h5': return 2; case 'h6': return 1; case 'hgroup': for (var i = 1; i <= 6; i ++) { if (element.getElementsByTagName('h' + i).length) { return 7 - i; } } return 6; } return 0; } function isHeading(element) { return testElementName(element, /^(h[1-6]|hgroup)$/); } function isSectioningContent(element) { return testElementName(element, /^(article|aside|nav|section)$/); } function isSectioningRoot(element) { return testElementName(element, /^(blockquote|body|figure|td)$/); } function testElementName(element, regex) { return regex.test(element.tagName.toLowerCase()) } } jetpack.future.import('storage.simple'); jetpack.future.import('slideBar'); jetpack.slideBar.append({ onSelect: function(aSlidebar) { var outline = OutlineService.createOutline(jetpack.tabs.focused.contentDocument); var body = aSlidebar.contentDocument.getElementsByTagName('body')[0]; var range = aSlidebar.contentDocument.createRange(); range.selectNodeContents(body); range.deleteContents(body); range.insertNode(outline); range.detach(); }, width : 200, html: <>
> }); jetpack.tabs.onReady(function(aDocument) { OutlineService.initDocument(aDocument); }); if (jetpack.storage.simple.showInContent) { jetpack.storage.simple.showInContent.forEach(function(aValue){ OutlineService.showInContent = aValue; }); } var checked = OutlineService.showInContent ? 'checked="true"' : '' ; jetpack.statusBar.append({ html: '', width : 70, onReady: function(aWidget){ $('input', aWidget).click(function(){ OutlineService.showInContent = this.checked; jetpack.storage.simple.showInContent = [this.checked]; }); } });