1
+ if ( window . hsInEditor ) {
2
+ /* While in the editor add styling to improve the appearance of code-blocks within the editor. */
3
+ let template = document . createElement ( 'style' ) ;
4
+ template . innerHTML = `
5
+ /* Content editor only styles injected only when the component is viewed in the page editor. */
6
+ .hs-inline-edit .hs-embed-wrapper[data-mce-embed-content~="<code-block"] .hs-embed-content-wrapper {
7
+ display: block;
8
+ background: rgb(45, 45, 45);
9
+ min-height: 20px;
10
+ padding: 10px;
11
+ line-height:1;
12
+ color: rgb(204, 204, 204);
13
+ }
14
+ .hs-inline-edit .hs-embed-wrapper[data-mce-embed-content~="<code-block"] .hs-embed-content-wrapper:before {
15
+ content:"Please make sure to escape HTML and HubL before using in blog posts.";
16
+ display:block;
17
+ position:relative;
18
+ color:red;
19
+ font-weight: bold;
20
+ }
21
+ .hs-inline-edit .hs-embed-wrapper[data-mce-embed-content~="<code-block"]:hover .hs-embed-content-wrapper:after {
22
+ content:"Use the post preview to see an accurate depiction of how the code block will display." ;
23
+ display:block;
24
+ font-size:.9em;
25
+ position:relative;
26
+ color:gray;
27
+ font-style:italic;
28
+ }
29
+
30
+ ` ;
31
+ document . querySelector ( 'head' ) . appendChild ( template ) ;
32
+ }
33
+
34
+ // change string into escaped characters to prevent rendering DOM elements. This is used for the contents of the code blocks if the user doesn't specify that the content is escaped.
35
+ function encode ( str ) {
36
+ var buf = [ ] ;
37
+ for ( var i = str . length - 1 ; i >= 0 ; i -- ) {
38
+ buf . unshift ( [ "&#" , str [ i ] . charCodeAt ( ) , ";" ] . join ( "" ) ) ;
39
+ }
40
+ return buf . join ( "" ) ;
41
+ }
42
+ // load stylesheet to be used by the web component, used for syntax highlighting.
43
+ import syntaxHighlighting from "{{ get_asset_url('../css/components/prism.css')}}" assert { type : "css " } ;
44
+
45
+
46
+ // Register <code-tab> element
47
+ class CodeTab extends HTMLElement {
48
+ constructor ( ) {
49
+ super ( ) ;
50
+ this . attachShadow ( { mode : "open" } ) ;
51
+ }
52
+
53
+ connectedCallback ( ) {
54
+ // pass Syntax highlighting stylesheets to be used in the ShadowDOM for <code-tab> elements.
55
+ this . shadowRoot . adoptedStyleSheets = [ syntaxHighlighting ] ;
56
+ // Get attributes from <code-tab> element needed for prism and escaping.
57
+ let language = this . getAttribute ( "data-language" ) ;
58
+ let lineNumbers = this . getAttribute ( "data-line-numbers" ) == "false" ? "no-line-numbers" : "line-numbers" ;
59
+ let isEscaped = this . getAttribute ( "data-escaped" ) ;
60
+ // take whatever code is between <code-tab> and </code-tab> and escape it unless the data-escaped attribute is set to true.
61
+ let code ;
62
+ if ( isEscaped == "true" ) {
63
+ code = this . innerHTML . replace ( / ^ \s + / g, "" ) ; // replace strips the leading whitespace
64
+
65
+ } else {
66
+ code = encode ( this . innerHTML . replace ( / ^ \s + / g, "" ) ) ; // replace strips the leading whitespace
67
+ }
68
+
69
+ // The Shadow DOM HTML for a <code-tab> element.
70
+ this . shadowRoot . innerHTML = `
71
+ <style>
72
+ :host{display:block;}
73
+ :host code[class*="language-"],:host pre[class*="language-"]{margin-top:0;}
74
+ </style>
75
+ <div class="code-snippet">
76
+ <pre class="${ lineNumbers } "><code class="language-${ language } ">${ code } </code></pre>
77
+ </div>
78
+ ` ;
79
+ // once the web component is initialized tell Prism to syntax highlight the code blocks.
80
+ Prism . highlightAllUnder ( this . shadowRoot ) ;
81
+ }
82
+ }
83
+ window . customElements . define ( "code-tab" , CodeTab ) ;
84
+
85
+ // <code-block> custom element - This element is where you place <code-tab> elements into.
86
+ class CodeBlock extends HTMLElement {
87
+ constructor ( ) {
88
+ super ( ) ;
89
+ // attach a ShadowDom to the <code-block> element. The mode makes it so JavaScript outside the root can interact with it's contents.
90
+ this . attachShadow ( { mode : "open" } ) ;
91
+ }
92
+ connectedCallback ( ) {
93
+
94
+ // check if code-tab element has data-line-numbers set to false. This value gets used to pass the line numbers option to
95
+ /* let lineNumbers = this.getAttribute("data-line-numbers") == "false" ? "no-line-numbers": "line-numbers"; */
96
+ // let codeTabs = this.querySelectorAll("code-tab");
97
+ const codeBlockElement = this ; // the element itself
98
+ const shadowRoot = codeBlockElement . shadowRoot ;
99
+ // The ShadowDOM for the <code-block>. The tablist, is where we will place buttons for the user to tab through.
100
+ // <slot> determines where <code-tab> elements will be rendered.
101
+ shadowRoot . innerHTML = `
102
+ <style>
103
+ .code-block__tablist button{
104
+ appearance:none;
105
+ border-width:0;
106
+ background:rgb(153, 153, 153);
107
+ padding:15px;
108
+ color:#fff;
109
+ }
110
+ .code-block__tablist button[aria-selected="true"]{
111
+ background:rgb(45, 45, 45);
112
+ }
113
+ </style>
114
+ <section class="code-block">
115
+ <div class="code-block__tablist" role="tablist" aria-label="Code Example"></div>
116
+ <slot></slot>
117
+ </section>
118
+ ` ;
119
+ let codeTabs = codeBlockElement . querySelectorAll ( "code-tab" ) ;
120
+
121
+
122
+ // for each <code-tab> element create a corresponding <button> in a tablist div.
123
+ codeTabs . forEach ( function ( individualCodeTab , i ) {
124
+ let tabButton ;
125
+ // get the button label from the <code-tab> element
126
+ let buttonLabel = individualCodeTab . getAttribute ( "data-label" ) ;
127
+
128
+ // update <code-tab> elements to have corresponding IDs and aria labels to ensure accessibility.
129
+ individualCodeTab . setAttribute ( "aria-labelledby" , `tab-${ i } ` ) ;
130
+ individualCodeTab . setAttribute ( "id" , `tabPanel-${ i } ` ) ;
131
+
132
+ // if the <button> is for the first <code-tab>, set aria-selected to true because it will be the active tab initially.
133
+ if ( i == 0 ) {
134
+ tabButton = `<button role="tab" aria-selected="true" id="tab-${ i } " aria-controls="tabPanel-${ i } ">${ buttonLabel } </button>` ;
135
+
136
+ } else {
137
+ tabButton = `<button role="tab" id="tab-${ i } " aria-controls="tabPanel-${ i } ">${ buttonLabel } </button>` ;
138
+ // set all of the tabs after the first one to hidden initially.
139
+ individualCodeTab . hidden = true ;
140
+ }
141
+
142
+ // add button element within the tablist div.
143
+ shadowRoot . querySelector ( ".code-block__tablist" ) . innerHTML += tabButton ;
144
+ } ) ;
145
+
146
+ /* handle tabs clicks and panel visibility */
147
+ let tabButtons = shadowRoot . querySelectorAll ( "button[role='tab']" ) ;
148
+ //let tabPanels = shadowRoot.querySelectorAll("code-tab"); duplicate of codeTabs
149
+
150
+ // for every tabButton click, remove aria-selected for all tabs, add aria-selected to clicked tab
151
+ tabButtons . forEach ( function ( tabButton , i ) {
152
+
153
+ tabButton . addEventListener ( "click" , function ( ) {
154
+ let clickedTabButton = this ;
155
+ let activeTabPanelId = clickedTabButton . getAttribute ( "aria-controls" ) ;
156
+ let activeCodeTab = codeBlockElement . querySelector ( `#${ activeTabPanelId } ` ) ;
157
+ // remove aria-selected from all tabs
158
+ tabButtons . forEach ( function ( tabButton ) {
159
+ tabButton . ariaSelected = false ;
160
+
161
+ } ) ;
162
+ // add aria-selected to clicked tab.
163
+ clickedTabButton . ariaSelected = true ;
164
+
165
+ // hide all <code-tab>'s that are not active.
166
+ codeBlockElement . querySelectorAll ( `code-tab:not(#${ activeTabPanelId } )` ) . forEach ( function ( inactiveTab ) {
167
+ inactiveTab . hidden = true ;
168
+ } ) ;
169
+
170
+ // ensure the active <code-tab> is not hidden.
171
+ activeCodeTab . hidden = false ;
172
+ } ) ;
173
+ } ) ;
174
+ }
175
+ }
176
+ window . customElements . define ( "code-block" , CodeBlock ) ;
177
+
178
+
179
+ /*
180
+ Example of how you'd use this:
181
+ <code-block data-line-numbers="true">
182
+ <code-tab data-language="HTML" data-escaped="true">
183
+ <h1>Your HTML here. Any HTML or HubL should be escaped</h1>
184
+ </code-tab>
185
+ </code-block>
186
+
187
+ If multiple code-tabs exist inside code-block it will display tabs for the different languages.
188
+ data-escaped is optional, you are telling the component that your code is already escaped so it doesn't need to be escaped using JavaScript.
189
+ It is recommended to always escape html otherwise it technically gets added to the DOM temporarily.
190
+ data-language is where you set the language.
191
+ */
0 commit comments