From 6b6626c966134b011bb7e5e1e50a70345ec87eb3 Mon Sep 17 00:00:00 2001
From: Ryo Hilmawan <54142180+CloudHill@users.noreply.github.com>
Date: Mon, 23 Nov 2020 14:00:59 +0700
Subject: [PATCH] upload outliner extension

---
 mods/outliner/app.css    |  51 +++++++++++++++++
 mods/outliner/icon.svg   |   8 +++
 mods/outliner/mod.js     |  23 ++++++++
 mods/outliner/panel.html |   2 +
 mods/outliner/panel.js   | 115 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 199 insertions(+)
 create mode 100644 mods/outliner/app.css
 create mode 100644 mods/outliner/icon.svg
 create mode 100644 mods/outliner/mod.js
 create mode 100644 mods/outliner/panel.html
 create mode 100644 mods/outliner/panel.js

diff --git a/mods/outliner/app.css b/mods/outliner/app.css
new file mode 100644
index 0000000..700dcff
--- /dev/null
+++ b/mods/outliner/app.css
@@ -0,0 +1,51 @@
+/*
+ * outliner
+ * (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (c) 2020 CloudHill
+ * under the MIT license
+ */
+ 
+.outliner {
+  max-height: 100%;
+  overflow: hidden auto;
+}
+
+.outline-header {
+  display: flex;
+  align-items: center;
+  height: 2.2em;
+  cursor: pointer;
+  user-select: none;
+  transition: background 20ms ease-in;
+}
+.outline-header:hover {
+  background: var(--theme--interactive_hover);
+}
+
+.outline-header a {
+  width: 100%;
+  height: 100%;
+  padding: 0 14px;
+  line-height: 2.2;
+  color: inherit;
+  text-decoration: none;
+  
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.outline-header a:empty:before {
+  color: var(--theme--text_ui_info);
+  content: attr(placeholder);
+  display: block;
+}
+.outline-header.notion-header-block a {
+  text-indent: 0;
+}
+.outline-header.notion-sub_header-block a {
+  text-indent: 18px;
+}
+.outline-header.notion-sub_sub_header-block a {
+  text-indent: 36px;
+}
\ No newline at end of file
diff --git a/mods/outliner/icon.svg b/mods/outliner/icon.svg
new file mode 100644
index 0000000..30f8901
--- /dev/null
+++ b/mods/outliner/icon.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+	<circle cx="5" cy="7" r="2.8"/>
+	<circle cx="5" cy="17" r="2.79"/>
+	<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
+	<path d="M17,10.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,9.6,17.55,10.05,17,10.05z"/>
+	<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
+	<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
+</svg>
diff --git a/mods/outliner/mod.js b/mods/outliner/mod.js
new file mode 100644
index 0000000..5fa2272
--- /dev/null
+++ b/mods/outliner/mod.js
@@ -0,0 +1,23 @@
+/*
+ * outliner
+ * (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (c) 2020 CloudHill
+ * under the MIT license
+ */
+
+'use strict';
+
+module.exports = {
+  id: '87e077cc-5402-451c-ac70-27cc4ae65546',
+  tags: ['extension', 'panel'],
+  name: 'outliner',
+  desc: 'table of contents.',
+  version: '1.0.0',
+  author: 'CloudHill',
+  panel: {
+    html: "panel.html",
+    name: "Outline",
+    icon: "icon.svg",
+    js: "panel.js",
+  }
+};
diff --git a/mods/outliner/panel.html b/mods/outliner/panel.html
new file mode 100644
index 0000000..fa82ef7
--- /dev/null
+++ b/mods/outliner/panel.html
@@ -0,0 +1,2 @@
+<div class="outliner">
+</div>
\ No newline at end of file
diff --git a/mods/outliner/panel.js b/mods/outliner/panel.js
new file mode 100644
index 0000000..32ffa05
--- /dev/null
+++ b/mods/outliner/panel.js
@@ -0,0 +1,115 @@
+/*
+ * outliner
+ * (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (c) 2020 CloudHill
+ * under the MIT license
+ */
+
+'use strict';
+
+const { createElement } = require("../../pkg/helpers");
+
+module.exports = (store) => {    
+  function initOutliner() {
+    // Find headers when switching panels
+    if (document.querySelector('.notion-page-content')) {
+      startContentObserver();
+    };
+
+    // Observe for page changes
+    const pageObserver = new MutationObserver((list, observer) => {
+      for ( let { addedNodes } of list) {
+        if (addedNodes[0]) {
+          if (addedNodes[0].className === 'notion-page-content') {
+            startContentObserver();
+          }
+          // Clear outline on database pages
+          else if (addedNodes[0].className === 'notion-scroller') {
+            contentObserver.disconnect();
+            const outline = document.querySelector('.outliner');
+            if (outline) outline.textContent = '';
+          }
+        } 
+      }
+    });
+    pageObserver.observe(document.body, {
+      childList: true,
+      subtree: true,
+    });
+    
+    // Observe for header changes
+    const contentObserver = new MutationObserver((list, observer) => {
+      list.forEach(m => {
+        if (
+          (
+            m.type === 'childList' &&
+            (
+              isHeaderElement(m.target) ||
+              isHeaderElement(m.addedNodes[0]) ||
+              isHeaderElement(m.removedNodes[0])
+            )
+          ) ||
+          (
+            m.type === 'characterData' &&
+            isHeaderElement(m.target.parentElement)
+          )
+        ) findHeaders();
+      })
+    });
+    function startContentObserver() {
+      findHeaders();
+      contentObserver.disconnect();
+      contentObserver.observe(
+        document.querySelector('.notion-page-content'),
+        {
+          childList: true,
+          subtree: true,
+          characterData: true,
+        }
+      );
+    }
+  }
+
+  function findHeaders() {
+    const outline = document.querySelector('.outliner');
+    if (!outline) {
+      pageObserver.disconnect();
+      observer.disconnect();
+    }
+    outline.textContent = '';
+
+    const pageContent = document.querySelector('.notion-page-content');
+    const headerBlocks = pageContent.querySelectorAll('[class*="header-block"]');
+    
+    headerBlocks.forEach(block => {
+      const blockId = block.dataset.blockId.replace(/-/g, '');
+      const placeholder = block.querySelector('[placeholder]').getAttribute('placeholder');
+      const header = createElement(`
+        <div class="outline-header ${block.classList[1]}">
+          <a href="${window.location.pathname}#${blockId}"
+            placeholder="${placeholder}">${block.innerText}</a>
+        </div>
+      `);
+
+      outline.append(header);
+    })
+  }
+
+  function isHeaderElement(el) {
+    let placeholder;
+    if (el) {
+      if (
+        el.querySelector && 
+        el.querySelector('[placeholder]')
+      ) {
+        placeholder = el.querySelector('[placeholder]').getAttribute('placeholder')
+      } else if (el.getAttribute) {
+        placeholder = el.getAttribute('placeholder');
+      } 
+    }
+    if (!placeholder) placeholder = '';
+    return placeholder.includes('Heading');
+  }
+
+  return initOutliner;
+}
\ No newline at end of file