Skip to content

Commit e329c88

Browse files
authored
Merge pull request #209 from zvonimirfras/master
feat(code-snippet): add code-snippet
2 parents e7c616b + c3e8b89 commit e329c88

File tree

6 files changed

+359
-13
lines changed

6 files changed

+359
-13
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
2+
import { ComponentFixture, TestBed } from "@angular/core/testing";
3+
import { StaticIconModule } from "../icon/static-icon.module";
4+
import { I18nModule } from "../i18n/i18n.module";
5+
6+
import { CodeSnippet } from "./code-snippet.component";
7+
8+
describe("CodeSnippet", () => {
9+
let component: CodeSnippet;
10+
let fixture: ComponentFixture<CodeSnippet>;
11+
12+
beforeEach(() => {
13+
TestBed.configureTestingModule({
14+
declarations: [CodeSnippet],
15+
imports: [BrowserAnimationsModule, StaticIconModule, I18nModule]
16+
});
17+
18+
fixture = TestBed.createComponent(CodeSnippet);
19+
component = fixture.componentInstance;
20+
});
21+
22+
it("should work", () => {
23+
expect(component instanceof CodeSnippet).toBe(true);
24+
});
25+
});
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import {
2+
Component,
3+
Input,
4+
HostBinding,
5+
ViewChild,
6+
HostListener
7+
} from "@angular/core";
8+
9+
import { I18n } from "../i18n/i18n.module";
10+
11+
export enum SnippetType {
12+
single = "single",
13+
multi = "multi",
14+
inline = "inline"
15+
}
16+
17+
/**
18+
* ```html
19+
* <ibm-code-snippet>Code</ibm-code-snippet>
20+
* ```
21+
* @export
22+
* @class CodeSnippet
23+
*/
24+
@Component({
25+
selector: "ibm-code-snippet",
26+
template: `
27+
<ng-container *ngIf="display === 'inline'; else notInline">
28+
<ng-container *ngTemplateOutlet="codeTemplate"></ng-container>
29+
<ng-container *ngTemplateOutlet="feedbackTemplate"></ng-container>
30+
</ng-container>
31+
32+
<ng-template #notInline>
33+
<div class="bx--snippet-container" [attr.aria-label]="translations.CODE_SNIPPET_TEXT">
34+
<pre><ng-container *ngTemplateOutlet="codeTemplate"></ng-container></pre>
35+
</div>
36+
<button
37+
class="bx--snippet-button"
38+
[attr.aria-label]="translations.COPY_CODE"
39+
(click)="onCopyButtonClicked()"
40+
tabindex="0">
41+
<svg class="bx--snippet__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
42+
<path d="M1 10H0V2C0 .9.9 0 2 0h8v1H2c-.6 0-1 .5-1 1v8z" />
43+
<path d="M11 4.2V8h3.8L11 4.2zM15 9h-4c-.6 0-1-.4-1-1V4H4.5c-.3 0-.5.2-.5.5v10c0 .3.2.5.5.5h10c.3 0
44+
.5-.2.5-.5V9zm-4-6c.1 0 .3.1.4.1l4.5 4.5c0 .1.1.3.1.4v6.5c0 .8-.7 1.5-1.5 1.5h-10c-.8
45+
0-1.5-.7-1.5-1.5v-10C3 3.7 3.7 3 4.5 3H11z"/>
46+
</svg>
47+
<ng-container *ngTemplateOutlet="feedbackTemplate"></ng-container>
48+
</button>
49+
<button
50+
*ngIf="display === 'multi' && shouldShowExpandButton"
51+
class="bx--btn bx--btn--ghost bx--btn--sm bx--snippet-btn--expand"
52+
(click)="toggleSnippetExpansion()"
53+
type="button">
54+
<span class="bx--snippet-btn--text">{{expanded ? translations.SHOW_LESS : translations.SHOW_MORE}}</span>
55+
<svg
56+
class="bx--icon-chevron--down"
57+
width="12" height="7"
58+
viewBox="0 0 12 7"
59+
[attr.aria-label]="translations.SHOW_MORE_ICON">
60+
<title>{{translations.SHOW_MORE_ICON}}</title>
61+
<path fill-rule="nonzero" d="M6.002 5.55L11.27 0l.726.685L6.003 7 0 .685.726 0z" />
62+
</svg>
63+
</button>
64+
</ng-template>
65+
66+
<ng-template #codeTemplate>
67+
<code #code><ng-content></ng-content></code>
68+
</ng-template>
69+
70+
<ng-template #feedbackTemplate>
71+
<div
72+
class="bx--btn--copy__feedback"
73+
[ngClass]="{
74+
'bx--btn--copy__feedback--displayed': showFeedback
75+
}"
76+
[attr.data-feedback]="feedbackText">
77+
</div>
78+
</ng-template>
79+
`
80+
})
81+
export class CodeSnippet {
82+
/**
83+
* Variable used for creating unique ids for code-snippet components.
84+
* @type {number}
85+
* @static
86+
* @memberof CodeSnippet
87+
*/
88+
static codeSnippetCount = 0;
89+
90+
/**
91+
* It can be `"single"`, `"multi"` or `"inline"`
92+
*
93+
* @type {SnippetType}
94+
* @memberof CodeSnippet
95+
*/
96+
@Input() display: SnippetType = SnippetType.single;
97+
@Input() translations = this.i18n.get().CODE_SNIPPET;
98+
99+
/**
100+
* Text displayed in the tooltip when user clicks button to copy code.
101+
*
102+
* @memberof CodeSnippet
103+
*/
104+
@Input() feedbackText = this.translations.COPIED;
105+
106+
/**
107+
* Time in miliseconds to keep the feedback tooltip displayed.
108+
*
109+
* @memberof CodeSnippet
110+
*/
111+
@Input() feedbackTimeout = 2000;
112+
113+
@HostBinding("class.bx--snippet--expand") @Input() expanded = false;
114+
115+
@HostBinding("class.bx--snippet") snippetClass = true;
116+
@HostBinding("class.bx--snippet--single") get snippetSingleClass() {
117+
return this.display === SnippetType.single;
118+
}
119+
@HostBinding("class.bx--snippet--multi") get snippetMultiClass() {
120+
return this.display === SnippetType.multi;
121+
}
122+
@HostBinding("class.bx--snippet--inline") get snippetInlineClass() {
123+
return this.display === SnippetType.inline;
124+
}
125+
@HostBinding("class.bx--btn--copy") get btnCopyClass() {
126+
return this.display === SnippetType.inline;
127+
}
128+
129+
@HostBinding("style.display") get displayStyle() {
130+
return this.display !== SnippetType.inline ? "block" : null;
131+
}
132+
@HostBinding("attr.type") get attrType() {
133+
return this.display === SnippetType.inline ? "button" : null;
134+
}
135+
136+
@ViewChild("code") code;
137+
138+
get shouldShowExpandButton() {
139+
return this.code ? this.code.nativeElement.getBoundingClientRect().height > 255 : false;
140+
}
141+
142+
showFeedback = false;
143+
144+
/**
145+
* Creates an instance of CodeSnippet.
146+
* @param {ChangeDetectorRef} changeDetectorRef
147+
* @param {ElementRef} elementRef
148+
* @param {Renderer2} renderer
149+
* @memberof CodeSnippet
150+
*/
151+
constructor(protected i18n: I18n) {
152+
CodeSnippet.codeSnippetCount++;
153+
}
154+
155+
toggleSnippetExpansion() {
156+
this.expanded = !this.expanded;
157+
}
158+
159+
/**
160+
* Copies the code from the `<code>` block to clipboard.
161+
*
162+
* @memberof CodeSnippet
163+
*/
164+
copyCode() {
165+
// create invisible, uneditable textarea with our code in it
166+
const textarea = document.createElement("textarea");
167+
textarea.value = this.code.nativeElement.innerText || this.code.nativeElement.textContent;
168+
textarea.setAttribute("readonly", "");
169+
textarea.style.position = "absolute";
170+
textarea.style.right = "-99999px";
171+
document.body.appendChild(textarea);
172+
173+
// save user selection
174+
const selected = document.getSelection().rangeCount ? document.getSelection().getRangeAt(0) : null;
175+
176+
// copy to clipboard
177+
textarea.select();
178+
document.execCommand("copy");
179+
180+
// remove textarea
181+
document.body.removeChild(textarea);
182+
183+
// restore user selection
184+
if (selected) {
185+
document.getSelection().removeAllRanges();
186+
document.getSelection().addRange(selected);
187+
}
188+
}
189+
190+
onCopyButtonClicked() {
191+
this.copyCode();
192+
193+
this.showFeedback = true;
194+
195+
setTimeout(() => {
196+
this.showFeedback = false;
197+
}, this.feedbackTimeout);
198+
}
199+
200+
/**
201+
* Inline code snippet acts as button and makes the whole component clickable.
202+
*
203+
* This handles clicks in that case.
204+
*
205+
* @returns
206+
* @memberof CodeSnippet
207+
*/
208+
@HostListener("click")
209+
hostClick() {
210+
if (this.display !== SnippetType.inline) {
211+
return;
212+
}
213+
214+
this.onCopyButtonClicked();
215+
}
216+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// modules
2+
import { NgModule } from "@angular/core";
3+
import { FormsModule } from "@angular/forms";
4+
import { CommonModule } from "@angular/common";
5+
6+
import { I18nModule } from "../i18n/i18n.module";
7+
8+
// imports
9+
import { CodeSnippet } from "./code-snippet.component";
10+
11+
// exports
12+
export { CodeSnippet } from "./code-snippet.component";
13+
14+
@NgModule({
15+
declarations: [
16+
CodeSnippet
17+
],
18+
exports: [
19+
CodeSnippet
20+
],
21+
imports: [
22+
CommonModule,
23+
FormsModule,
24+
I18nModule
25+
]
26+
})
27+
export class CodeSnippetModule { }
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { storiesOf, moduleMetadata } from "@storybook/angular";
2+
import { withKnobs } from "@storybook/addon-knobs/angular";
3+
4+
import { CodeSnippetModule } from "..";
5+
6+
7+
const code = `import { storiesOf, moduleMetadata } from "@storybook/angular";
8+
import { withKnobs, boolean } from "@storybook/addon-knobs/angular";
9+
10+
import { CodeSnippetModule } from "..";
11+
12+
storiesOf("CodeSnippet", module).addDecorator(
13+
moduleMetadata({
14+
imports: [CodeSnippetModule]
15+
})
16+
)
17+
.addDecorator(withKnobs)
18+
.add("Basic", () => ({
19+
template: \`<ibm-code-snippet>code</ibm-code-snippet>\`,
20+
props: { // there's more
21+
// disabled: boolean("disabled", false)
22+
}
23+
}));`;
24+
25+
const lessCode = `import { storiesOf, moduleMetadata } from "@storybook/angular";
26+
import { withKnobs, boolean } from "@storybook/addon-knobs/angular";
27+
28+
import { CodeSnippetModule } from "..";
29+
30+
storiesOf("Code Snippet", module).addDecorator(
31+
moduleMetadata({
32+
imports: [CodeSnippetModule]
33+
})
34+
) // that's it, no more after this line
35+
`;
36+
37+
const inlineCode = "<inline code>";
38+
39+
storiesOf("CodeSnippet", module).addDecorator(
40+
moduleMetadata({
41+
imports: [CodeSnippetModule]
42+
})
43+
)
44+
.addDecorator(withKnobs)
45+
.add("Basic", () => ({
46+
template: `<ibm-code-snippet display="single">{{code}}</ibm-code-snippet>`,
47+
props: {
48+
code
49+
}
50+
}))
51+
.add("Multi", () => ({
52+
template: `
53+
<h2>With a lot of code</h2>
54+
<ibm-code-snippet display="multi">{{code}}</ibm-code-snippet>
55+
56+
<h2 style="margin-top: 60px">With less code</h2>
57+
<ibm-code-snippet display="multi">{{lessCode}}</ibm-code-snippet>
58+
`,
59+
props: {
60+
code,
61+
lessCode
62+
}
63+
}))
64+
.add("Inline", () => ({
65+
template: `Here is some <ibm-code-snippet display="inline">{{inlineCode}}</ibm-code-snippet> for you.`,
66+
props: {
67+
inlineCode
68+
}
69+
}));

src/i18n/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@
4747
}
4848
]
4949
},
50+
"CODE_SNIPPET": {
51+
"CODE_SNIPPET_TEXT": "Code Snippet Text",
52+
"SHOW_MORE": "Show more",
53+
"SHOW_LESS": "Show less",
54+
"SHOW_MORE_ICON": "Show more icon",
55+
"COPY_CODE": "Copy code",
56+
"COPIED": "Copied!"
57+
},
5058
"DIALOG": {
5159
"POPOVER" : {
5260
"CLOSE": "Close popover"

src/index.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1+
export * from "./accordion/accordion.module";
12
export * from "./banner/banner.module";
23
export * from "./button-menu/button-menu.module";
4+
export * from "./button/button.module";
35
export * from "./calendar/calendar.module";
6+
export * from "./checkbox/checkbox.module";
7+
export * from "./code-snippet/code-snippet.module";
48
export * from "./combobox/combobox.module";
9+
export * from "./content-switcher/content-switcher.module";
510
export * from "./dialog/dialog.module";
611
export * from "./dropdown/dropdown.module";
712
export * from "./forms/forms.module";
13+
export * from "./i18n/i18n.module";
814
export * from "./icon/icon.module";
15+
export * from "./input/input.module";
916
export * from "./list-group/list-group.module";
17+
export * from "./loading/loading.module";
1018
export * from "./modal/modal.module";
11-
export * from "./table/table.module";
12-
export * from "./tabs/tabs.module";
19+
export * from "./pagination/pagination.module";
1320
export * from "./pill-input/pill-input.module";
14-
export * from "./utils/position";
15-
export * from "./content-switcher/content-switcher.module";
16-
export * from "./accordion/accordion.module";
17-
export * from "./button/button.module";
18-
export * from "./checkbox/checkbox.module";
19-
export * from "./switch/switch.module";
21+
export * from "./progress-indicator/progress-indicator.module";
2022
export * from "./radio/radio.module";
21-
export * from "./input/input.module";
2223
export * from "./select/select.module";
24+
export * from "./switch/switch.module";
25+
export * from "./table/table.module";
26+
export * from "./tabs/tabs.module";
2327
export * from "./tiles/tiles.module";
24-
export * from "./progress-indicator/progress-indicator.module";
25-
export * from "./i18n/i18n.module";
26-
export * from "./pagination/pagination.module";
27-
export * from "./loading/loading.module";
28+
export * from "./utils/position";

0 commit comments

Comments
 (0)