cssghost-blogprismjs

How to style prismjs copy to clipboard button?


I want to copy the exact code block design designed by Ghost using prismjs.

Here is the link - https://ghost.org/tutorials/code-snippets-in-ghost/

The end result must look like the below screenshot.

enter image description here

What I tried looks like this.

enter image description here

Link for the settings I enabled for the code block formatting in prismjs.

https://prismjs.com/download.html#themes=prism-okaidia&languages=css+clike+javascript+css-extras&plugins=autolinker+custom-class+show-language+inline-color+autoloader+toolbar+copy-to-clipboard+match-braces


Solution

  • Solution 1:

    If you want exactly the same styles as on the page

    https://ghost.org/tutorials/code-snippets-in-ghost/

    here is a instruction to get their theme file, download and name it prism.css

    1. Open the 'Sources' tab in the DevTools
    2. Find and click on the /tutorials/assets/built/components/syntax-highlighting.css file
    3. Right click on a tab with content of syntax-highlighting.css
    4. Save as... prism.css

    Snippet with all rules including copy button:

    (the button is not shown in the snippet when using prism.js from CDN)

    Similarity is achieved by adding css rules, but there is no animation. enter image description here

    /**
     * MIT License
     * Copyright (c) 2018 Sarah Drasner
     * Sarah Drasner's[@sdras] Night Owl
     * Ported by Sara vieria [@SaraVieira]
     * Added by Souvik Mandal [@SimpleIndian]
     */
    
     code[class*="language-"],
     pre[class*="language-"] {
         font-family: var(--font-mono);
         font-size: inherit;
         hyphens: none;
         line-height: 1.6;
         color: #d6deeb;
         text-align: left;
         word-break: normal;
         word-wrap: normal;
         tab-size: 4;
         tab-size: 4;
         tab-size: 4;
         white-space: pre;
         word-spacing: normal;
         -webkit-font-smoothing: auto;
     }
     
     pre[class*="language-"]::selection,
     pre[class*="language-"] ::selection,
     code[class*="language-"]::selection,
     code[class*="language-"] ::selection {
         text-shadow: none;
         background: rgb(29 59 83 / 99%);
     }
     
     pre[class*="language-"]::selection,
     pre[class*="language-"] ::selection,
     code[class*="language-"]::selection,
     code[class*="language-"] ::selection {
         text-shadow: none;
         background: rgb(29 59 83 / 99%);
     }
     
     @media print {
         code[class*="language-"],
         pre[class*="language-"] {
             text-shadow: none;
         }
     }
     
     /* Code blocks */
     pre[class*="language-"] {
         padding: 25px;
         margin-top: 3.2vmin;
         overflow: auto;
     }
     
     :not(pre) > code[class*="language-"],
     pre[class*="language-"] {
         color: white;
         background: #011627;
     }
     
     :not(pre) > code[class*="language-"] {
         padding: 0.1em;
         white-space: normal;
         border-radius: 0.3em;
     }
     
     .token.comment,
     .token.prolog,
     .token.cdata {
         font-style: italic;
         color: rgb(99 119 119);
     }
     
     .token.punctuation {
         color: rgb(199 146 234);
     }
     
     .namespace {
         color: rgb(178 204 214);
     }
     
     .token.deleted {
         font-style: italic;
         color: rgb(239 83 80 / 56%);
     }
     
     .token.symbol,
     .token.property {
         color: rgb(128 203 196);
     }
     
     .token.tag,
     .token.operator,
     .token.keyword {
         color: rgb(127 219 202);
     }
     
     .token.boolean {
         color: rgb(255 88 116);
     }
     
     .token.number {
         color: rgb(247 140 108);
     }
     
     .token.constant,
     .token.function,
     .token.builtin,
     .token.char {
         color: rgb(130 170 255);
     }
     
     .token.selector,
     .token.doctype {
         font-style: italic;
         color: rgb(199 146 234);
     }
     
     .token.attr-name,
     .token.inserted {
         font-style: italic;
         color: rgb(173 219 103);
     }
     
     .token.string,
     .token.url,
     .token.entity,
     .language-css .token.string,
     .style .token.string {
         color: rgb(173 219 103);
     }
     
     .token.class-name,
     .token.atrule,
     .token.attr-value {
         color: rgb(255 203 139);
     }
     
     .token.regex,
     .token.important,
     .token.variable {
         color: rgb(214 222 235);
     }
     
     .token.important,
     .token.bold {
         font-weight: 700;
     }
     
     .token.italic {
         font-style: italic;
     }
     
     /* Custom styles for theme */
     .code-wrapper {
         position: relative;
         overflow: hidden;
         border-radius: 8px;
     }
     
     .code-wrapper > pre[class*="language-"] {
         margin-top: 0;
     }
    
    
    div.code-toolbar{
        position:relative
    }
    div.code-toolbar>.toolbar{
        position:absolute;
        z-index:10;
        top:.3em;
        right:.2em;
        transition:opacity .3s ease-in-out;
        opacity:1
    }
    
    div.code-toolbar>.toolbar>.toolbar-item{
        display:inline-block
    }
    div.code-toolbar>.toolbar>.toolbar-item>a{
        cursor:pointer
    }
    
    /* feature styles for buttons */
    
    /* icon copy */
    div.code-toolbar>.toolbar>.toolbar-item>button > span
    {
        color: transparent;
        background-color:#e6caa8;
        mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'%3E%3C/path%3E%3Cpath d='M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
        mask-repeat: no-repeat no-repeat;
        mask-position: center right;
    }
    
    /* hide icon copy */
    div.code-toolbar>.toolbar>.toolbar-item>button[data-copy-state=copy-success] > span ,
    div.code-toolbar>.toolbar>.toolbar-item>button[data-copy-state=copy-success]:hover > span
    {
        mask-image: none;
        color: #e6caa8;
        background-color: transparent;
    }
    
    /* color of "Copied!" */
    .copy-to-clipboard-button[data-copy-state~=copy-success] span {
        color: #e6caa8;
    }
    
    /* text copy is hidden */
    .copy-to-clipboard-button[data-copy-state=copy] span {
        color: transparent;
    }
    
    /* default item color */
    div.code-toolbar>.toolbar>.toolbar-item {
       color: #e6caa8; 
    }
    
    /* remove toolbar items decoration*/
    div.code-toolbar > .toolbar > .toolbar-item > a, 
    div.code-toolbar > .toolbar > .toolbar-item > button, 
    div.code-toolbar > .toolbar > .toolbar-item > span {
        background-color: transparent;
        border: 0;
        box-shadow: none;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" integrity="sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <pre>
    <code class="language-css">
        .token.boolean,
        .token.number,
        .token.function {
                color: #f08d49;
        }
    </code>
    </pre>

    Solution 2:

    The most interesting part of the code came from researching the sources on ghost.org (button event: /tutorials/src/js/post/code-copy.js) and the sources of the buttons on github, however, in order to make it work, I had to make a symbiosis of two scripts because it doesn't work as it should ( functions copyTextToClipboard, fallbackCopyTextToClipboard from github converted to async).

    Now copy button looks and works like the original:

    enter image description here

    function initCodeCopy() {
        const codeBlocks = document.querySelectorAll('code[class*="language-"]');
        codeBlocks.forEach((block) => {
          const lang = parseLanguage(block);
          const referenceEl = block.parentElement;
          const parent = block.parentElement.parentElement;
          
          const wrapper = document.createElement('div');
          wrapper.className = 'code-wrapper';
          parent.insertBefore(wrapper, referenceEl);
          wrapper.append(block.parentElement);
    
          const copyBtn = document.createElement('button');
          copyBtn.setAttribute('class', 'copy-button');
          copyBtn.setAttribute('data-lang', lang);
          copyBtn.innerHTML = `${lang} <svg viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z" fill="currentColor"/></svg>`;
    
          wrapper.insertAdjacentElement('beforeend', copyBtn);
        });
    
        function parseLanguage(block) {
          const className = block.className;
          if (className.startsWith('language')) {
            const [prefix, lang] = className.split('-');
            return lang;
          }
        }
    
            async function fallbackCopyTextToClipboard(text) {
                return new Promise((resolve, reject) => {
                    var textArea = document.createElement('textarea');
                    textArea.value = copyInfo.getText();
                    // Avoid scrolling to bottom
                    textArea.style.top = '0';
                    textArea.style.left = '0';
                    textArea.style.position = 'fixed';
                    document.body.appendChild(textArea);
                    textArea.focus();
                    textArea.select();
                    try {
                        var successful = document.execCommand('copy');
                        setTimeout(function () {
                            if (successful) {
                                resolve('success')
                            } else {
                                reject('error')
                            }
                        }, 1);
                    } catch (err) {
                        setTimeout(function () {
                            reject(err)
                        }, 1);
                    } finally {
                        document.body.removeChild(textArea);
                    }
                })  
            }
        
            async function copyTextToClipboard(text) {
                return new Promise((resolve, reject) => {
                    if (navigator.clipboard) {
                        navigator.clipboard.writeText(text).then(
                            resolve(), function () {
                            // try the fallback in case `writeText` didn't work
                            fallbackCopyTextToClipboard(text).then(
                                () => resolve(),
                                () => reject()
                            )
                        });
                    } else {
                        fallbackCopyTextToClipboard(text).then(
                            () => resolve(),
                            () => reject()
                        )
                    }
                })
            }
    
        function copy(e) {
            const btn = e.currentTarget;
            const lang = btn.dataset.lang;
            const text = e.currentTarget.previousSibling.children[0].textContent;
            copyTextToClipboard(text)
                .then(
                () => {
                    btn.innerHTML = `copied! <svg viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zm2 0h8v10h2V4H9v2z" fill="currentColor"/></svg>`;
                    btn.setAttribute('style', 'opacity: 1');
                    
                },
                () => alert('failed to copy'),
            );
    
            setTimeout(() => {
                btn.removeAttribute('style');
                btn.innerHTML = `${lang} <svg viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z" fill="currentColor"/></svg>`;
            }, 3000);
        }
    
        const copyButtons = document.querySelectorAll('.copy-button');
    
        copyButtons.forEach((btn) => {
            btn.addEventListener('click', copy);
        });
        }
        initCodeCopy()
    /**
     * MIT License
     * Copyright (c) 2018 Sarah Drasner
     * Sarah Drasner's[@sdras] Night Owl
     * Ported by Sara vieria [@SaraVieira]
     * Added by Souvik Mandal [@SimpleIndian]
     */
    * {
      margin:0; /* make bottom corners round */
    }
    
    code[class*="language-"],
    pre[class*="language-"] {
        font-family: var(--font-mono);
        font-size: inherit;
        hyphens: none;
        line-height: 1.6;
        color: #d6deeb;
        text-align: left;
        word-break: normal;
        word-wrap: normal;
        tab-size: 4;
        tab-size: 4;
        tab-size: 4;
        white-space: pre;
        word-spacing: normal;
        -webkit-font-smoothing: auto;
    }
    
    pre[class*="language-"]::selection,
    pre[class*="language-"] ::selection,
    code[class*="language-"]::selection,
    code[class*="language-"] ::selection {
        text-shadow: none;
        background: rgb(29 59 83 / 99%);
    }
    
    pre[class*="language-"]::selection,
    pre[class*="language-"] ::selection,
    code[class*="language-"]::selection,
    code[class*="language-"] ::selection {
        text-shadow: none;
        background: rgb(29 59 83 / 99%);
    }
    
    @media print {
        code[class*="language-"],
        pre[class*="language-"] {
            text-shadow: none;
        }
    }
    
    /* Code blocks */
    pre[class*="language-"] {
        padding: 25px;
        margin-top: 3.2vmin;
        overflow: auto;
    }
    
    :not(pre) > code[class*="language-"],
    pre[class*="language-"] {
        color: white;
        background: #011627;
    }
    
    :not(pre) > code[class*="language-"] {
        padding: 0.1em;
        white-space: normal;
        border-radius: 0.3em;
    }
    
    .token.comment,
    .token.prolog,
    .token.cdata {
        font-style: italic;
        color: rgb(99 119 119);
    }
    
    .token.punctuation {
        color: rgb(199 146 234);
    }
    
    .namespace {
        color: rgb(178 204 214);
    }
    
    .token.deleted {
        font-style: italic;
        color: rgb(239 83 80 / 56%);
    }
    
    .token.symbol,
    .token.property {
        color: rgb(128 203 196);
    }
    
    .token.tag,
    .token.operator,
    .token.keyword {
        color: rgb(127 219 202);
    }
    
    .token.boolean {
        color: rgb(255 88 116);
    }
    
    .token.number {
        color: rgb(247 140 108);
    }
    
    .token.constant,
    .token.function,
    .token.builtin,
    .token.char {
        color: rgb(130 170 255);
    }
    
    .token.selector,
    .token.doctype {
        font-style: italic;
        color: rgb(199 146 234);
    }
    
    .token.attr-name,
    .token.inserted {
        font-style: italic;
        color: rgb(173 219 103);
    }
    
    .token.string,
    .token.url,
    .token.entity,
    .language-css .token.string,
    .style .token.string {
        color: rgb(173 219 103);
    }
    
    .token.class-name,
    .token.atrule,
    .token.attr-value {
        color: rgb(255 203 139);
    }
    
    .token.regex,
    .token.important,
    .token.variable {
        color: rgb(214 222 235);
    }
    
    .token.important,
    .token.bold {
        font-weight: 700;
    }
    
    .token.italic {
        font-style: italic;
    }
    
    /* Custom styles for theme */
    .code-wrapper {
        position: relative;
        overflow: hidden;
        border-radius: 8px;
    }
    
    .code-wrapper > pre[class*="language-"] {
        margin-top: 0;
    }
    
    .copy-button {
        position: absolute;
        top: 5px;
        right: 5px;
        display: flex;
        align-items: center;
        color: rgb(230 202 168);
        cursor: pointer;
        background: transparent;
        background-color: #011627;
        border: none;
    }
    
    .copy-button svg {
        width: 1em;
        margin-left: 0.25em;
        opacity: 0.5;
        transition: opacity 0.3s;
    }
    
    .copy-button:hover svg {
        opacity: 1;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" integrity="sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <pre class="language-css"><code class="language-css">.token.boolean,
        .token.number,
        .token.function {
                color: #f08d49;
        }
    </code></pre>