code.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. const codeActionButtons = [
  2. {
  3. icon: 'copy',
  4. id: 'copy',
  5. title: 'Copy Code',
  6. show: true
  7. },
  8. {
  9. icon: 'order',
  10. id: 'lines',
  11. title: 'Toggle Line Numbers',
  12. show: true
  13. },
  14. {
  15. icon: 'carly',
  16. id: 'wrap',
  17. title: 'Toggle Line Wrap',
  18. show: false
  19. },
  20. {
  21. icon: 'expand',
  22. id: 'expand',
  23. title: 'Toggle code block expand',
  24. show: false
  25. }
  26. ];
  27. const body = elem('body');
  28. const maxLines = parseInt(body.dataset.code);
  29. const copyId = 'panel_copy';
  30. const wrapId = 'panel_wrap';
  31. const linesId = 'panel_lines';
  32. const panelExpand = 'panel_expand';
  33. const panelExpanded = 'panel_expanded';
  34. const panelHide = 'panel_hide';
  35. const panelFrom = 'panel_from';
  36. const panelBox = 'panel_box';
  37. const fullHeight = 'initial';
  38. const highlightWrap = 'highlight_wrap'
  39. function wrapOrphanedPreElements() {
  40. const pres = elems('pre');
  41. Array.from(pres).forEach(function(pre){
  42. const parent = pre.parentNode;
  43. const isOrpaned = !containsClass(parent, 'highlight');
  44. if(isOrpaned) {
  45. const preWrapper = createEl();
  46. preWrapper.className = 'highlight';
  47. const outerWrapper = createEl();
  48. outerWrapper.className = highlightWrap;
  49. wrapEl(pre, preWrapper);
  50. wrapEl(preWrapper, outerWrapper);
  51. }
  52. })
  53. /*
  54. @Todo
  55. 1. Add UI control to orphaned blocks
  56. */
  57. }
  58. wrapOrphanedPreElements();
  59. function codeBlocks() {
  60. const markedCodeBlocks = elems('code');
  61. const blocks = Array.from(markedCodeBlocks).filter(function(block){
  62. return hasClasses(block) && !Array.from(block.classList).includes('noClass');
  63. }).map(function(block){
  64. return block
  65. });
  66. return blocks;
  67. }
  68. function codeBlockFits(block) {
  69. // return false if codeblock overflows
  70. const blockWidth = block.offsetWidth;
  71. const highlightBlockWidth = block.parentNode.parentNode.offsetWidth;
  72. return blockWidth <= highlightBlockWidth ? true : false;
  73. }
  74. function maxHeightIsSet(elem) {
  75. let maxHeight = elem.style.maxHeight;
  76. return maxHeight.includes('px')
  77. }
  78. function restrainCodeBlockHeight(lines) {
  79. const lastLine = lines[maxLines-1];
  80. let maxCodeBlockHeight = fullHeight;
  81. if(lastLine) {
  82. const lastLinePos = lastLine.offsetTop;
  83. if(lastLinePos !== 0) {
  84. maxCodeBlockHeight = `${lastLinePos}px`;
  85. const codeBlock = lines[0].parentNode;
  86. const outerBlock = codeBlock.closest('.highlight');
  87. const isExpanded = containsClass(outerBlock, panelExpanded);
  88. if(!isExpanded) {
  89. codeBlock.dataset.height = maxCodeBlockHeight;
  90. codeBlock.style.maxHeight = maxCodeBlockHeight;
  91. }
  92. }
  93. }
  94. }
  95. const blocks = codeBlocks();
  96. function collapseCodeBlock(block) {
  97. const lines = elems('.ln', block);
  98. const codeLines = lines.length;
  99. if (codeLines > maxLines) {
  100. const expandDot = createEl()
  101. pushClass(expandDot, panelExpand);
  102. pushClass(expandDot, panelFrom);
  103. expandDot.title = "Toggle code block expand";
  104. expandDot.textContent = "...";
  105. const outerBlock = block.closest('.highlight');
  106. window.setTimeout(function(){
  107. const expandIcon = outerBlock.nextElementSibling.lastElementChild;
  108. deleteClass(expandIcon, panelHide);
  109. }, 150)
  110. restrainCodeBlockHeight(lines);
  111. const highlightElement = block.parentNode.parentNode;
  112. highlightElement.appendChild(expandDot);
  113. }
  114. }
  115. blocks.forEach(function(block){
  116. collapseCodeBlock(block);
  117. })
  118. function actionPanel() {
  119. const panel = createEl();
  120. panel.className = panelBox;
  121. codeActionButtons.forEach(function(button) {
  122. // create button
  123. const btn = createEl('a');
  124. btn.href = '#';
  125. btn.title = button.title;
  126. btn.className = `icon panel_icon panel_${button.id}`;
  127. button.show ? false : pushClass(btn, panelHide);
  128. // load icon inside button
  129. loadSvg(button.icon, btn);
  130. // append button on panel
  131. panel.appendChild(btn);
  132. });
  133. return panel;
  134. }
  135. function toggleLineNumbers(elems) {
  136. elems.forEach(function (elem, index) {
  137. // mark the code element when there are no lines
  138. modifyClass(elem, 'pre_nolines')
  139. });
  140. restrainCodeBlockHeight(elems);
  141. }
  142. function toggleLineWrap(elem) {
  143. modifyClass(elem, 'pre_wrap');
  144. // retain max number of code lines on line wrap
  145. const lines = elems('.ln', elem);
  146. restrainCodeBlockHeight(lines);
  147. }
  148. function copyCode(codeElement) {
  149. lineNumbers = elems('.ln', codeElement);
  150. // remove line numbers before copying
  151. if(lineNumbers.length) {
  152. lineNumbers.forEach(function(line){
  153. line.remove();
  154. });
  155. }
  156. const codeToCopy = codeElement.textContent;
  157. // copy code
  158. copyToClipboard(codeToCopy);
  159. }
  160. function disableCodeLineNumbers(block){
  161. const lines = elems('.ln', block)
  162. toggleLineNumbers(lines);
  163. }
  164. (function codeActions(){
  165. const blocks = codeBlocks();
  166. const highlightWrapId = highlightWrap;
  167. blocks.forEach(function(block){
  168. // disable line numbers if disabled globally
  169. const showLines = elem('body').dataset.lines;
  170. parseBoolean(showLines) === false ? disableCodeLineNumbers(block) : false;
  171. const highlightElement = block.parentNode.parentNode;
  172. // wrap code block in a div
  173. const highlightWrapper = createEl();
  174. highlightWrapper.className = highlightWrapId;
  175. wrapEl(highlightElement, highlightWrapper);
  176. const panel = actionPanel();
  177. // show wrap icon only if the code block needs wrapping
  178. const wrapIcon = elem(`.${wrapId}`, panel);
  179. codeBlockFits(block) ? false : deleteClass(wrapIcon, panelHide);
  180. // append buttons
  181. highlightWrapper.appendChild(panel);
  182. });
  183. function isItem(target, id) {
  184. // if is item or within item
  185. return target.matches(`.${id}`) || target.closest(`.${id}`);
  186. }
  187. function showActive(target, targetClass,activeClass = 'active') {
  188. const active = activeClass;
  189. const targetElement = target.matches(`.${targetClass}`) ? target : target.closest(`.${targetClass}`);
  190. deleteClass(targetElement, active);
  191. setTimeout(function() {
  192. modifyClass(targetElement, active)
  193. }, 50)
  194. }
  195. doc.addEventListener('click', function(event){
  196. // copy code block
  197. const target = event.target;
  198. const isCopyIcon = isItem(target, copyId);
  199. const isWrapIcon = isItem(target, wrapId);
  200. const isLinesIcon = isItem(target, linesId);
  201. const isExpandIcon = isItem(target, panelExpand);
  202. const isActionable = isCopyIcon || isWrapIcon || isLinesIcon || isExpandIcon;
  203. if(isActionable) {
  204. event.preventDefault();
  205. showActive(target, 'icon');
  206. const codeElement = target.closest(`.${highlightWrapId}`).firstElementChild.firstElementChild;
  207. let lineNumbers = elems('.ln', codeElement);
  208. isWrapIcon ? toggleLineWrap(codeElement) : false;
  209. isLinesIcon ? toggleLineNumbers(lineNumbers) : false;
  210. if (isExpandIcon) {
  211. let thisCodeBlock = codeElement.firstElementChild;
  212. const outerBlock = thisCodeBlock.closest('.highlight');
  213. if(maxHeightIsSet(thisCodeBlock)) {
  214. thisCodeBlock.style.maxHeight = fullHeight;
  215. // mark code block as expanded
  216. pushClass(outerBlock, panelExpanded)
  217. } else {
  218. thisCodeBlock.style.maxHeight = thisCodeBlock.dataset.height;
  219. // unmark code block as expanded
  220. deleteClass(outerBlock, panelExpanded)
  221. }
  222. }
  223. if(isCopyIcon) {
  224. // clone code element
  225. const codeElementClone = codeElement.cloneNode(true);
  226. copyCode(codeElementClone);
  227. }
  228. }
  229. });
  230. (function addLangLabel() {
  231. const blocks = codeBlocks();
  232. blocks.forEach(function(block){
  233. let label = block.dataset.lang;
  234. label = label === 'sh' ? 'bash' : label;
  235. if(label !== "fallback") {
  236. const labelEl = createEl();
  237. labelEl.textContent = label;
  238. pushClass(labelEl, 'lang');
  239. block.closest(`.${highlightWrap}`).appendChild(labelEl);
  240. }
  241. });
  242. })();
  243. })();