diff options
1 files changed, 255 insertions, 0 deletions
| -rw-r--r-- | assets/js/code.js | 255 |
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); + } + }); + })(); +})(); |
