Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fifty-tips-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stackoverflow/stacks-editor": minor
---

Adds show/hide functionality to Snippets
1 change: 1 addition & 0 deletions plugins/official/stack-snippets/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export const stackSnippetRichTextNodeSpec: { [name: string]: NodeSpec } = {
babel: { default: "null" },
babelPresetReact: { default: "null" },
babelPresetTS: { default: "null" },
showCode: { default: true },
},
},
stack_snippet_lang: {
Expand Down
66 changes: 64 additions & 2 deletions plugins/official/stack-snippets/src/snippet-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,64 @@ export class StackSnippetView implements NodeView {
this.getPos = getPos;

this.snippetMetadata = getSnippetMetadata(node);
const codeIsShown: boolean =
typeof node.attrs.showCode === "boolean"
? node.attrs.showCode
: true;

//We want to render the language blocks in the middle of some content,
// so we need to custom-render stuff here ("holes" must be last)
const snippetContainer = document.createElement("div");
this.dom = snippetContainer;
snippetContainer.className = "snippet";

let toggleContainer: HTMLDivElement;

if (this.snippetMetadata.hide === "true") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches current functionality. Out of scope for this PR, but I wonder if we could improve on what's here by always showing the show/hide accordian rather than keying it off of the snippet config (and instead setting the default based on that config value)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought this behavior was interesting but I spoke to @KyleMit and it seems like users want to be able to control how the final post will look and if the snippet will have a show/hide link above it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what @threefjefff is still inline with my use cases (although agreed this matches existing behavior). It wouldn't bug me if the link to be able to show/hide was always visible, but the default respected the value set by the config in the post.

That way OP has first say in whether it should be collapsed or visible, but users could also quickly collapse existing snippets in answers

// Create the show/hide link container
toggleContainer = document.createElement("div");
toggleContainer.className =
"snippet-toggle-container d-inline-flex ai-center g2";

// Create the arrow span
const arrowSpan = document.createElement("span");
arrowSpan.className = codeIsShown
? "svg-icon-bg iconArrowDownSm"
: "svg-icon-bg iconArrowRightSm";
toggleContainer.appendChild(arrowSpan);

// Create the show/hide link
const toggleLink = document.createElement("a");
toggleLink.href = "#";
toggleLink.className = "snippet-toggle fs-body1";
toggleLink.textContent = codeIsShown
? "Hide code snippet"
: "Show code snippet";
toggleContainer.appendChild(toggleLink);

snippetContainer.appendChild(toggleContainer);
}

//This is the div where we're going to render any language blocks
const snippetCode = document.createElement("div");
snippetCode.className = "snippet-code";
snippetCode.style.display = codeIsShown ? "" : "none";
snippetContainer.appendChild(snippetCode);
this.contentDOM = snippetCode;

if (this.snippetMetadata.hide === "true") {
toggleContainer.addEventListener("click", (e) => {
e.preventDefault();
const isVisible = snippetCode.style.display !== "none";
this.view.dispatch(
this.view.state.tr.setNodeMarkup(this.getPos(), null, {
...node.attrs,
showCode: !isVisible,
})
);
});
}

//And this is where we stash our CTAs and results, which are statically rendered.
const snippetResult = document.createElement("div");
snippetResult.className = "snippet-result";
Expand Down Expand Up @@ -122,6 +167,23 @@ export class StackSnippetView implements NodeView {
JSON.stringify(this.snippetMetadata);
this.snippetMetadata = updatedMeta;

if (this.snippetMetadata.hide === "true") {
// Update the visibility of the snippet-code div and toggle link
const snippetCode = this.contentDOM;
const toggleLink = this.dom.querySelector(".snippet-toggle");
const arrowSpan = this.dom.querySelector(".svg-icon-bg");

const isVisible = node.attrs.showCode as boolean;
snippetCode.style.display = isVisible ? "" : "none";
toggleLink.textContent = isVisible
? "Hide code snippet"
: "Show code snippet";
arrowSpan.className = isVisible
? "svg-icon-bg iconArrowDownSm"
: "svg-icon-bg iconArrowRightSm";
}

// Update the result container if metadata has changed
const content = this.contentNode;
if (metaChanged && content) {
//Clear the node
Expand All @@ -134,7 +196,7 @@ export class StackSnippetView implements NodeView {
"allow-forms allow-modals allow-scripts"
);
if (content.nodeType === Node.DOCUMENT_NODE) {
const document = <Document>content;
const document = content as Document;
iframe.srcdoc = document.documentElement.innerHTML;
}
this.resultContainer.appendChild(iframe);
Expand All @@ -148,6 +210,6 @@ export class StackSnippetView implements NodeView {
private contentNode: Node;
private getPos: () => number;
resultContainer: HTMLDivElement;
dom: Node;
dom: HTMLElement;
contentDOM: HTMLElement;
}
65 changes: 52 additions & 13 deletions plugins/official/stack-snippets/test/snippet-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@ describe("StackSnippetView", () => {
{ language: "js" },
schema.text("console.log('test');")
);
const validSnippet = schema.nodes.stack_snippet.createChecked(
{
id: "1234",
babel: "true",
babelPresetReact: "true",
babelPresetTS: "null",
console: "true",
hide: "false",
},
langNode
);
const validSnippet = (hide: string) =>
schema.nodes.stack_snippet.createChecked(
{
id: "1234",
babel: "true",
babelPresetReact: "true",
babelPresetTS: "null",
console: "true",
hide: hide,
},
langNode
);

const buildView = (options?: StackSnippetOptions): EditorView => {
const buildView = (
options?: StackSnippetOptions,
hide: string = "false"
): EditorView => {
const state = EditorState.create({
schema: schema,
plugins: [stackSnippetPasteHandler],
Expand All @@ -48,7 +52,7 @@ describe("StackSnippetView", () => {
view.state.tr.replaceRangeWith(
0,
view.state.doc.nodeSize - 2,
validSnippet
validSnippet(hide)
)
);

Expand Down Expand Up @@ -128,4 +132,39 @@ describe("StackSnippetView", () => {
const [resultDiv] = resultDoc.getElementsByTagName("div");
expect(resultDiv.textContent).toBe("test!");
});

it("should render show/hide link if hide attr is true", () => {
const view = buildView(undefined, "true");
const toggleLink = view.dom.querySelectorAll(".snippet-toggle");

expect(toggleLink).toHaveLength(1);
expect(toggleLink[0].textContent).toBe("Hide code snippet");
});

it("should not render show/hide link if hide attr is false", () => {
const view = buildView(undefined, "false");
const toggleLink = view.dom.querySelectorAll(".snippet-toggle");

expect(toggleLink).toHaveLength(0);
});

it("should toggle visibility of code snippet when show/hide link is clicked", () => {
const view = buildView(undefined, "true");
const toggleLink = view.dom.querySelector(".snippet-toggle");
const snippetCode = view.dom.querySelector(".snippet-code");

// Initial state: Code is visible
expect((<HTMLDivElement>snippetCode).style.display).toBe("");
expect(toggleLink.textContent).toBe("Hide code snippet");

// Click to hide
(<HTMLAnchorElement>toggleLink).click();
expect((<HTMLDivElement>snippetCode).style.display).toBe("none");
expect(toggleLink.textContent).toBe("Show code snippet");

// Click to show
(<HTMLAnchorElement>toggleLink).click();
expect((<HTMLDivElement>snippetCode).style.display).toBe("");
expect(toggleLink.textContent).toBe("Hide code snippet");
});
});
7 changes: 7 additions & 0 deletions src/styles/icons.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
.svg-icon-bg.iconEllipsisHorizontal {
--bg-icon: url("~@stackoverflow/stacks-icons/src/Icon/EllipsisHorizontal.svg");
}

.svg-icon-bg.iconArrowDownSm {
--bg-icon: url("~@stackoverflow/stacks-icons/src/Icon/ArrowDownSm.svg");
}
.svg-icon-bg.iconArrowRightSm {
--bg-icon: url("~@stackoverflow/stacks-icons/src/Icon/ArrowRightSm.svg");
}
.svg-icon-bg.iconSearchSm {
width: 21px;
--bg-icon: url("~@stackoverflow/stacks-icons/src/Icon/SearchSm.svg");
Expand Down
Loading