Website Summary Files Commits Branches @commit @diff @stats
path: root / assets / js / code.js
diff options
context:
space:
mode:
1 files changed, 255 insertions, 0 deletions
-rw-r--r--assets/js/code.js255
diff --git a/assets/js/code.js b/assets/js/code.js
new file mode 100644
index 0000000..afa546e
--- /dev/null
+++ b/assets/js/code.js
@@ -0,0 +1,255 @@
+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 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);
+ }
+ });
+ })();
+})();