| New file |
| | |
| | | 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); |
| | | } |
| | | }); |
| | | })(); |
| | | })(); |