From 413e03f10099d7c341bd76337b6892e0893ae445 Mon Sep 17 00:00:00 2001
From: weru <fromweru@gmail.com>
Date: Sun, 27 Dec 2020 16:56:34 +0000
Subject: [PATCH] add helpers

---
 assets/js/index.js     |   23 ---
 assets/js/functions.js |   44 +++++++
 assets/js/code.js      |  277 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 322 insertions(+), 22 deletions(-)

diff --git a/assets/js/code.js b/assets/js/code.js
new file mode 100644
index 0000000..7116657
--- /dev/null
+++ b/assets/js/code.js
@@ -0,0 +1,277 @@
+const codeActionButtons = [
+  {
+    icon: 'copy', 
+    id: 'copy',
+    title: 'Copy Code',
+    show: true
+  },
+  {
+    icon: 'order',
+    id: 'lines',
+    title: 'Toggle Line Numbers',
+    show: true 
+  },
+  {
+    icon: 'carly',
+    id: 'wrap',
+    title: 'Toggle Line Wrap',
+    show: false
+  },
+  {
+    icon: 'expand',
+    id: 'expand',
+    title: 'Toggle code block expand',
+    show: false 
+  }
+];
+
+const body = elem('body');
+const maxLines = parseInt(body.dataset.code);
+const copyId = 'panel_copy';
+const wrapId = 'panel_wrap';
+const linesId = 'panel_lines';
+const panelExpand = 'panel_expand';
+const panelExpanded = 'panel_expanded';
+const panelHide = 'panel_hide';
+const panelFrom = 'panel_from';
+const panelBox = 'panel_box';
+const fullHeight = 'initial';
+const highlightWrap = 'highlight_wrap'
+
+function wrapOrphanedPreElements() {
+  const pres = elems('pre');
+  Array.from(pres).forEach(function(pre){
+    const parent = pre.parentNode;
+    const isOrpaned = !containsClass(parent, 'highlight');
+    if(isOrpaned) {
+      const preWrapper = createEl();
+      preWrapper.className = 'highlight';
+      const outerWrapper = createEl();
+      outerWrapper.className = highlightWrap;
+      wrapEl(pre, preWrapper);
+      wrapEl(preWrapper, outerWrapper);
+    }
+  })
+  /*
+  @Todo
+  1. Add UI control to orphaned blocks
+  */
+}
+
+wrapOrphanedPreElements();
+
+function codeBlocks() {
+  const markedCodeBlocks = elems('code');
+  const blocks = Array.from(markedCodeBlocks).filter(function(block){
+    return hasClasses(block) && !Array.from(block.classList).includes('noClass');
+  }).map(function(block){
+    return block
+  });
+  return blocks;
+}
+
+function codeBlockFits(block) {
+  // return false if codeblock overflows
+  const blockWidth = block.offsetWidth;
+  const highlightBlockWidth = block.parentNode.parentNode.offsetWidth;
+  return blockWidth <= highlightBlockWidth ? true : false;
+}
+
+function maxHeightIsSet(elem) {
+  let maxHeight = elem.style.maxHeight;
+  return maxHeight.includes('px')
+}
+
+function restrainCodeBlockHeight(lines) {
+  const lastLine = lines[maxLines-1];
+  let maxCodeBlockHeight = fullHeight;
+  if(lastLine) {
+    const lastLinePos = lastLine.offsetTop;
+    if(lastLinePos !== 0) {
+      maxCodeBlockHeight = `${lastLinePos}px`;
+      const codeBlock = lines[0].parentNode;
+      const outerBlock = codeBlock.closest('.highlight');
+      const isExpanded = containsClass(outerBlock, panelExpanded);
+      if(!isExpanded) {
+        codeBlock.dataset.height = maxCodeBlockHeight;
+        codeBlock.style.maxHeight = maxCodeBlockHeight;
+      }
+    }
+  }
+}
+
+const blocks = codeBlocks();
+
+function collapseCodeBlock(block) {
+  const lines = elems('.ln', block);
+  const codeLines = lines.length;
+  if (codeLines > maxLines) {
+    const expandDot = createEl()
+    pushClass(expandDot, panelExpand);
+    pushClass(expandDot, panelFrom);
+    expandDot.title = "Toggle code block expand";
+    expandDot.textContent = "...";
+    const outerBlock = block.closest('.highlight');
+    window.setTimeout(function(){
+      const expandIcon = outerBlock.nextElementSibling.lastElementChild;
+      deleteClass(expandIcon, panelHide);
+    }, 150)
+
+    restrainCodeBlockHeight(lines);
+    const highlightElement = block.parentNode.parentNode;
+    highlightElement.appendChild(expandDot);
+  }
+}
+
+blocks.forEach(function(block){
+  collapseCodeBlock(block);
+})
+
+function actionPanel() {
+  const panel = createEl();
+  panel.className = panelBox;
+
+  codeActionButtons.forEach(function(button) {
+    // create button
+    const btn = createEl('a');
+    btn.href = '#';
+    btn.title = button.title;
+    btn.className = `icon panel_icon panel_${button.id}`;
+    button.show ? false : pushClass(btn, panelHide);
+    // load icon inside button
+    loadSvg(button.icon, btn);
+    // append button on panel
+    panel.appendChild(btn);
+  });
+
+  return panel;
+}
+
+function toggleLineNumbers(elems) {
+  elems.forEach(function (elem, index) {
+    // mark the code element when there are no lines
+    modifyClass(elem, 'pre_nolines')
+  });
+  restrainCodeBlockHeight(elems);
+}
+
+function toggleLineWrap(elem) {
+  modifyClass(elem, 'pre_wrap');
+  // retain max number of code lines on line wrap
+  const lines = elems('.ln', elem);
+  restrainCodeBlockHeight(lines);
+}
+
+function copyCode(codeElement) {
+  lineNumbers = elems('.ln', codeElement);
+  // remove line numbers before copying
+  if(lineNumbers.length) {
+    lineNumbers.forEach(function(line){
+      line.remove();
+    });
+  }
+
+  const codeToCopy = codeElement.textContent;
+  // copy code
+  copyToClipboard(codeToCopy);
+}
+
+function disableCodeLineNumbers(block){
+  const lines = elems('.ln', block)
+  toggleLineNumbers(lines);
+}
+
+(function codeActions(){
+  const blocks = codeBlocks();
+
+  const highlightWrapId = highlightWrap;
+  blocks.forEach(function(block){
+    // disable line numbers if disabled globally
+    const showLines = elem('body').dataset.lines;
+    parseBoolean(showLines) === false ? disableCodeLineNumbers(block) : false;
+
+    const highlightElement = block.parentNode.parentNode;
+    // wrap code block in a div
+    const highlightWrapper = createEl();
+    highlightWrapper.className = highlightWrapId;
+    wrapEl(highlightElement, highlightWrapper);
+
+    const panel = actionPanel();
+    // show wrap icon only if the code block needs wrapping
+    const wrapIcon = elem(`.${wrapId}`, panel);
+    codeBlockFits(block) ? false : deleteClass(wrapIcon, panelHide);
+
+    // append buttons 
+    highlightWrapper.appendChild(panel);
+  });
+
+  function isItem(target, id) {
+    // if is item or within item
+    return target.matches(`.${id}`) || target.closest(`.${id}`);
+  }
+
+  function showActive(target, targetClass,activeClass = 'active') {
+    const active = activeClass;
+    const targetElement = target.matches(`.${targetClass}`) ? target : target.closest(`.${targetClass}`);
+
+    deleteClass(targetElement, active);
+    setTimeout(function() {
+      modifyClass(targetElement, active)
+    }, 50)
+  }
+
+  doc.addEventListener('click', function(event){
+    // copy code block
+    const target = event.target;
+    const isCopyIcon = isItem(target, copyId);
+    const isWrapIcon = isItem(target, wrapId);
+    const isLinesIcon = isItem(target, linesId);
+    const isExpandIcon = isItem(target, panelExpand);
+    const isActionable = isCopyIcon || isWrapIcon || isLinesIcon || isExpandIcon;
+
+    if(isActionable) {
+      event.preventDefault();
+      showActive(target, 'icon');
+      const codeElement = target.closest(`.${highlightWrapId}`).firstElementChild.firstElementChild;
+      let lineNumbers = elems('.ln', codeElement);
+
+      isWrapIcon ? toggleLineWrap(codeElement) : false;
+
+      isLinesIcon ? toggleLineNumbers(lineNumbers) : false;
+
+      if (isExpandIcon) {
+        let thisCodeBlock = codeElement.firstElementChild;
+        const outerBlock = thisCodeBlock.closest('.highlight');
+        if(maxHeightIsSet(thisCodeBlock)) {
+          thisCodeBlock.style.maxHeight = fullHeight;
+          // mark code block as expanded
+          pushClass(outerBlock, panelExpanded)
+        } else {
+          thisCodeBlock.style.maxHeight = thisCodeBlock.dataset.height;
+          // unmark code block as expanded
+          deleteClass(outerBlock, panelExpanded)
+        }
+      }
+
+      if(isCopyIcon) {
+        // clone code element
+        const codeElementClone = codeElement.cloneNode(true);
+        copyCode(codeElementClone);
+      }
+    }
+  });
+
+  (function addLangLabel() {
+    const blocks = codeBlocks();
+    blocks.forEach(function(block){
+      let label = block.dataset.lang;
+      label = label === 'sh' ? 'bash' : label;
+      if(label !== "fallback") {
+        const labelEl = createEl();
+        labelEl.textContent = label;
+        pushClass(labelEl, 'lang');
+        block.closest(`.${highlightWrap}`).appendChild(labelEl);
+      }
+    });
+  })();
+})();
diff --git a/assets/js/functions.js b/assets/js/functions.js
index 33a15e9..d5bba50 100644
--- a/assets/js/functions.js
+++ b/assets/js/functions.js
@@ -151,4 +151,48 @@
       link.href = href;
     });
   }
+}
+
+function parseBoolean(string) {
+  let bool;
+  string = string.trim().toLowerCase();
+  switch (string) {
+    case 'true':
+      return true;
+    case 'false':
+      return false;
+    default:
+      return undefined;
+  }
+};
+
+function loadSvg(file, parent, path = 'icons/') {
+  const link = `${parentURL}${path}${file}.svg`;
+  fetch(link)
+  .then((response) => {
+    return response.text();
+  })
+  .then((data) => {
+    parent.innerHTML = data;
+  });
+}
+
+function copyToClipboard(str) {
+  let copy, selection, selected;
+  copy = createEl('textarea');
+  copy.value = str;
+  copy.setAttribute('readonly', '');
+  copy.style.position = 'absolute';
+  copy.style.left = '-9999px';
+  selection = document.getSelection();
+  doc.appendChild(copy);
+  // check if there is any selected content
+  selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
+  copy.select();
+  document.execCommand('copy');
+  doc.removeChild(copy);
+  if (selected) { // if a selection existed before copying
+    selection.removeAllRanges(); // unselect existing selection
+    selection.addRange(selected); // restore the original selection
+  }
 }
\ No newline at end of file
diff --git a/assets/js/index.js b/assets/js/index.js
index c5f5842..a8e2792 100644
--- a/assets/js/index.js
+++ b/assets/js/index.js
@@ -183,27 +183,6 @@
     }
   });
 
-  const copyToClipboard = str => {
-    let copy, selection, selected;
-    copy = createEl('textarea');
-    copy.value = str;
-    copy.setAttribute('readonly', '');
-    copy.style.position = 'absolute';
-    copy.style.left = '-9999px';
-    selection = document.getSelection();
-    doc.appendChild(copy);
-    // check if there is any selected content
-    selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
-    copy.select();
-    document.execCommand('copy');
-    doc.removeChild(copy);
-    if (selected) { // if a selection existed before copying
-      selection.removeAllRanges(); // unselect existing selection
-      selection.addRange(selected); // restore the original selection
-    }
-  }
-
-
   function copyFeedback(parent) {
     const copyText = document.createElement('div');
     const yanked = 'link_yanked';
@@ -355,4 +334,4 @@
 
 }
 
-window.addEventListener('load', loadActions());
+window.addEventListener('load', loadActions());
\ No newline at end of file

--
Gitblit v1.10.0