diff --git a/docs/_layouts/view.html b/docs/_layouts/view.html
index ebe918b9..f3be90c2 100644
--- a/docs/_layouts/view.html
+++ b/docs/_layouts/view.html
@@ -8,7 +8,7 @@
-
+
diff --git a/docs/dist/v1/chartifact-toolbar.css b/docs/dist/v1/chartifact-toolbar.css
index 2e4e8848..402c025c 100644
--- a/docs/dist/v1/chartifact-toolbar.css
+++ b/docs/dist/v1/chartifact-toolbar.css
@@ -58,17 +58,17 @@
}
/* Default: stack toolbar sections vertically */
-.chartifact-toolbar > div {
+.chartifact-toolbar .toolbar-item {
margin: 6px 0;
}
/* Wide screens: layout horizontally */
@media (min-width: 1000px) {
- .chartifact-toolbar {
+ .chartifact-toolbar .toolbar-group {
display: flex;
justify-content: space-between;
}
- .chartifact-toolbar > div {
+ .chartifact-toolbar .toolbar-item {
display: inline-block;
margin: 0;
}
diff --git a/docs/dist/v1/chartifact.host.umd.js b/docs/dist/v1/chartifact.host.umd.js
index e6f58d62..903ea0a3 100644
--- a/docs/dist/v1/chartifact.host.umd.js
+++ b/docs/dist/v1/chartifact.host.umd.js
@@ -2363,6 +2363,162 @@ ${guardedJs}
return false;
}
}
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+ function decamelize(str2, separator = "-") {
+ return str2.replace(/([a-z\d])([A-Z])/g, "$1" + separator + "$2").replace(/([A-Z]+)([A-Z][a-z\d]+)/g, "$1" + separator + "$2").toLowerCase();
+ }
+ function createElement(tag, attrs, ...children) {
+ if (typeof tag === "function") {
+ const fn = tag;
+ let props = attrs;
+ if (props === null || props === void 0) {
+ props = { children };
+ } else {
+ props.children = children;
+ }
+ return fn(props);
+ } else {
+ const ns = tag === "svg" ? SVG_NAMESPACE : null;
+ const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
+ const map2 = attrs;
+ let ref;
+ for (let name in map2) {
+ if (name && map2.hasOwnProperty(name)) {
+ let value = map2[name];
+ if (name === "className" && value !== void 0) {
+ setAttribute(el, ns, "class", value.toString());
+ } else if (name === "disabled" && !value) ;
+ else if (value === null || value === void 0) {
+ continue;
+ } else if (value === true) {
+ setAttribute(el, ns, name, name);
+ } else if (typeof value === "function") {
+ if (name === "ref") {
+ ref = value;
+ } else {
+ el[name.toLowerCase()] = value;
+ }
+ } else if (typeof value === "object") {
+ setAttribute(el, ns, name, flatten(value));
+ } else {
+ setAttribute(el, ns, name, value.toString());
+ }
+ }
+ }
+ if (children && children.length > 0) {
+ appendChildren(el, children);
+ }
+ if (ref) {
+ ref(el);
+ }
+ return el;
+ }
+ }
+ function setAttribute(el, ns, name, value) {
+ if (ns) {
+ el.setAttributeNS(null, name, value);
+ } else {
+ el.setAttribute(name, value);
+ }
+ }
+ function flatten(o) {
+ const arr = [];
+ for (let prop in o)
+ arr.push(`${decamelize(prop, "-")}:${o[prop]}`);
+ return arr.join(";");
+ }
+ function isInsideForeignObject(element) {
+ let current = element;
+ while (current) {
+ if (current.tagName.toLowerCase() === "foreignobject") {
+ return true;
+ }
+ current = current.parentElement;
+ }
+ return false;
+ }
+ function recreateWithSvgNamespace(element) {
+ const svgElement = document.createElementNS(SVG_NAMESPACE, element.tagName.toLowerCase());
+ for (let i2 = 0; i2 < element.attributes.length; i2++) {
+ const attr = element.attributes[i2];
+ svgElement.setAttributeNS(null, attr.name, attr.value);
+ }
+ const eventProperties = [
+ "onclick",
+ "onmousedown",
+ "onmouseup",
+ "onmouseover",
+ "onmouseout",
+ "onmousemove",
+ "onkeydown",
+ "onkeyup",
+ "onkeypress",
+ "onfocus",
+ "onblur"
+ ];
+ for (const prop of eventProperties) {
+ if (element[prop]) {
+ svgElement[prop] = element[prop];
+ }
+ }
+ for (let i2 = 0; i2 < element.childNodes.length; i2++) {
+ const child = element.childNodes[i2];
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ svgElement.appendChild(recreateWithSvgNamespace(child));
+ } else {
+ svgElement.appendChild(child.cloneNode(true));
+ }
+ }
+ return svgElement;
+ }
+ function addChild(parentElement, child) {
+ if (child === null || child === void 0 || typeof child === "boolean") {
+ return;
+ } else if (Array.isArray(child)) {
+ appendChildren(parentElement, child);
+ } else if (isElement(child)) {
+ const childEl = child;
+ if (parentElement.namespaceURI === SVG_NAMESPACE && childEl.namespaceURI !== SVG_NAMESPACE && childEl.tagName.toLowerCase() !== "foreignobject" && !isInsideForeignObject(parentElement)) {
+ const recreated = recreateWithSvgNamespace(childEl);
+ parentElement.appendChild(recreated);
+ } else {
+ parentElement.appendChild(childEl);
+ }
+ } else {
+ parentElement.appendChild(document.createTextNode(child.toString()));
+ }
+ }
+ function appendChildren(parentElement, children) {
+ children.forEach((child) => addChild(parentElement, child));
+ }
+ function isElement(el) {
+ return !!el.nodeType;
+ }
+ function mount(element, container) {
+ container.innerHTML = "";
+ if (element) {
+ addChild(container, element);
+ }
+ }
+ const FolderDisplay = (props) => {
+ const { title, docIndex, docs, prevClick, nextClick } = props;
+ return /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement("span", null, title), /* @__PURE__ */ createElement("div", { style: { display: "inline-block", marginRight: "0.5em" } }), /* @__PURE__ */ createElement("span", null, "(document ", docIndex + 1, " of ", docs.length, ")"), /* @__PURE__ */ createElement("div", { style: { display: "inline-block" } }, /* @__PURE__ */ createElement("button", { type: "button", id: "prevBtn", disabled: docIndex === 0, onClick: prevClick }, "Previous"), /* @__PURE__ */ createElement(
+ "select",
+ {
+ id: "pageSelect",
+ title: `Select document (1 to ${docs.length})`,
+ onChange: (e) => props.pageSelectChange(parseInt(e.target.value, 10) - 1)
+ },
+ docs.map((doc, index2) => /* @__PURE__ */ createElement(
+ "option",
+ {
+ key: index2,
+ value: index2 + 1
+ },
+ doc.title ? doc.title : `Page ${index2 + 1}`
+ ))
+ ), /* @__PURE__ */ createElement("button", { type: "button", id: "nextBtn", disabled: docIndex === docs.length - 1, onClick: nextClick }, "Next")));
+ };
function loadFolder(folderUrl, folder, host) {
if (!folder || !folder.docs) {
host.errorHandler(
@@ -2386,27 +2542,6 @@ ${guardedJs}
return;
}
let docIndex = 0;
- const { folderSpan } = host.toolbar;
- folderSpan.style.display = "";
- const prevBtn = document.createElement("button");
- prevBtn.textContent = "Previous";
- prevBtn.disabled = docIndex === 0;
- const nextBtn = document.createElement("button");
- nextBtn.textContent = "Next";
- nextBtn.disabled = docIndex === folder.docs.length - 1;
- const pageSelect = document.createElement("select");
- for (let i2 = 0; i2 < folder.docs.length; i2++) {
- const option = document.createElement("option");
- option.value = (i2 + 1).toString();
- option.textContent = folder.docs[i2].title ? folder.docs[i2].title : `Page ${i2 + 1}`;
- pageSelect.appendChild(option);
- }
- pageSelect.value = (docIndex + 1).toString();
- const navDiv = document.createElement("div");
- navDiv.style.display = "inline-block";
- navDiv.appendChild(prevBtn);
- navDiv.appendChild(pageSelect);
- navDiv.appendChild(nextBtn);
function getHashParam(key) {
const params = new URLSearchParams(window.location.hash.slice(1));
return params.get(key) ?? void 0;
@@ -2416,47 +2551,39 @@ ${guardedJs}
params.set(key, value.toString());
window.location.hash = params.toString();
}
- function updateFolderTitle() {
- folderSpan.innerHTML = "";
- const label = document.createElement("span");
- label.textContent = `${folder.title} `;
- const docCountDiv = document.createElement("div");
- docCountDiv.style.display = "inline-block";
- docCountDiv.style.marginRight = "0.5em";
- docCountDiv.textContent = `(document ${docIndex + 1} of ${folder.docs.length}) `;
- folderSpan.appendChild(label);
- folderSpan.appendChild(docCountDiv);
- folderSpan.appendChild(navDiv);
- }
- updateFolderTitle();
+ const props = {
+ title: folder.title,
+ docIndex,
+ docs: folder.docs,
+ prevClick: () => {
+ if (docIndex > 0) {
+ updatePage(docIndex - 1, true);
+ }
+ },
+ nextClick: () => {
+ if (docIndex < folder.docs.length - 1) {
+ updatePage(docIndex + 1, true);
+ }
+ },
+ pageSelectChange: (index2) => {
+ if (index2 >= 0 && index2 <= folder.docs.length - 1) {
+ updatePage(index2, true);
+ }
+ }
+ };
function updatePage(newDocIndex, setHash = false) {
docIndex = newDocIndex;
if (setHash) {
setHashParam("page", docIndex + 1);
}
- prevBtn.disabled = docIndex === 0;
- nextBtn.disabled = docIndex === folder.docs.length - 1;
- pageSelect.value = (docIndex + 1).toString();
- updateFolderTitle();
+ host.toolbar.addChildren(/* @__PURE__ */ createElement(FolderDisplay, { ...{ ...props, docIndex } }));
+ const pageSelect = document.querySelector("#pageSelect");
+ if (pageSelect) {
+ pageSelect.value = (docIndex + 1).toString();
+ }
const title = folder.docs[docIndex].title || `Page ${docIndex + 1}`;
resolveUrl(title, folderUrl, folder.docs[docIndex].href, host);
}
- pageSelect.onchange = () => {
- const selectedPage = parseInt(pageSelect.value, 10);
- if (selectedPage >= 1 && selectedPage <= folder.docs.length) {
- updatePage(selectedPage - 1, true);
- }
- };
- prevBtn.onclick = () => {
- if (docIndex > 0) {
- updatePage(docIndex - 1, true);
- }
- };
- nextBtn.onclick = () => {
- if (docIndex < folder.docs.length - 1) {
- updatePage(docIndex + 1, true);
- }
- };
function goToPageFromHash() {
const pageStr = getHashParam("page");
let newIndex = 0;
@@ -2473,7 +2600,6 @@ ${guardedJs}
setHashParam("page", docIndex + 1);
}
goToPageFromHash();
- folderSpan.appendChild(navDiv);
}
async function resolveUrl(title, base, relativeOrAbsolute, host) {
let url;
@@ -3140,112 +3266,132 @@ ${htmlJsonJs}
htmlMarkdownWrapper,
htmlJsonWrapper
};
+ const ToolbarElement = (props) => {
+ const { mode, restartClick, tweakClick, downloadClick, restartDisplay, tweakDisplay, downloadDisplay, downloadSource, downloadHtml, children } = props;
+ const { home, target } = window.location.hostname === "localhost" ? { home: "/", target: "_self" } : { home: "https://microsoft.github.io/", target: "_blank" };
+ const displayMode = mode === "json" ? "json" : "markdown";
+ return createElement(
+ "div",
+ { className: "toolbar-group", style: { backgroundColor: "inherit" } },
+ createElement(
+ "div",
+ { className: "toolbar-item" },
+ createElement("a", { href: `${home}chartifact/`, target }, "Chartifact"),
+ " viewer"
+ ),
+ createElement("div", { className: "toolbar-item", style: { display: children ? "" : "none" } }, children),
+ createElement(
+ "div",
+ { className: "toolbar-item" },
+ createElement("button", { type: "button", id: "restart", style: { display: restartDisplay }, onClick: restartClick }, "start over"),
+ createElement("button", { type: "button", id: "tweak", style: { display: tweakDisplay }, onClick: tweakClick }, "view source"),
+ createElement("button", { type: "button", id: "download", style: { display: downloadDisplay }, onClick: downloadClick }, "download")
+ ),
+ createElement(
+ "div",
+ { id: "downloadPopup", style: {
+ position: "absolute",
+ display: "none",
+ padding: "12px 16px",
+ zIndex: 1,
+ backgroundColor: "inherit"
+ } },
+ createElement("div", { style: { marginBottom: "8px" } }, "Download as:"),
+ createElement(
+ "ul",
+ null,
+ createElement(
+ "li",
+ null,
+ "Source (just the ",
+ displayMode,
+ " content)",
+ createElement("br", null),
+ createElement("button", { type: "button", id: "download-md", style: { marginRight: "8px" }, onClick: downloadSource }, "Source")
+ ),
+ createElement(
+ "li",
+ null,
+ "HTML wrapper (content plus a shareable viewer)",
+ createElement("br", null),
+ createElement("button", { type: "button", id: "download-html", onClick: downloadHtml }, "HTML wrapper")
+ )
+ )
+ )
+ );
+ };
class Toolbar {
constructor(toolbarElementOrSelector, options = {}) {
__publicField(this, "options");
__publicField(this, "toolbarElement");
- __publicField(this, "folderSpan");
- __publicField(this, "tweakButton");
- __publicField(this, "restartButton");
__publicField(this, "downloadButton");
+ __publicField(this, "downloadPopup");
__publicField(this, "mode");
__publicField(this, "filename");
- __publicField(this, "downloadPopup");
- var _a, _b, _c, _d, _e;
+ __publicField(this, "props");
this.options = options;
this.filename = options.filename || "sample";
- this.mode = options.mode || "markdown";
+ const allowedModes = ["markdown", "json"];
+ this.mode = allowedModes.includes(options.mode) ? options.mode : "markdown";
this.toolbarElement = typeof toolbarElementOrSelector === "string" ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
if (!this.toolbarElement) {
throw new Error("Toolbar element not found");
}
- const { home, target } = window.location.hostname === "localhost" ? { home: "/", target: "_self" } : { home: "https://microsoft.github.io/", target: "_blank" };
- const html = `
-
-
-
-
-
-
-
-
- `;
- this.toolbarElement.innerHTML = html;
- this.folderSpan = this.toolbarElement.querySelector("#folderSpan");
- this.tweakButton = this.toolbarElement.querySelector("#tweak");
- this.restartButton = this.toolbarElement.querySelector("#restart");
- this.downloadButton = this.toolbarElement.querySelector("#download");
- this.downloadPopup = this.toolbarElement.querySelector("#downloadPopup");
- if (this.options.tweakButton) {
- this.showTweakButton();
- }
- if (this.options.restartButton) {
- this.showRestartButton();
- }
- if (this.options.downloadButton) {
- this.showDownloadButton();
- }
- (_a = this.tweakButton) == null ? void 0 : _a.addEventListener("click", () => {
- this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none";
- });
- (_b = this.restartButton) == null ? void 0 : _b.addEventListener("click", () => {
- window.location.reload();
- });
- (_c = this.downloadButton) == null ? void 0 : _c.addEventListener("click", (e) => {
- const rect = this.downloadButton.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const buttonCenter = rect.left + rect.width / 2;
- const isLeftOfCenter = buttonCenter < viewportWidth / 2;
- if (isLeftOfCenter) {
- this.downloadPopup.style.left = `${rect.left + window.scrollX}px`;
- this.downloadPopup.style.right = "";
- } else {
- this.downloadPopup.offsetWidth;
- this.downloadPopup.style.right = `${viewportWidth - rect.right - window.scrollX}px`;
- this.downloadPopup.style.left = "";
- }
- this.downloadPopup.style.top = `${rect.bottom + window.scrollY + 4}px`;
- this.downloadPopup.style.display = "block";
- const hidePopup = (evt) => {
- if (!this.downloadPopup.contains(evt.target) && evt.target !== this.downloadButton) {
- this.downloadPopup.style.display = "none";
- document.removeEventListener("mousedown", hidePopup);
+ this.props = {
+ mode: this.mode,
+ restartClick: () => window.location.reload(),
+ tweakClick: () => {
+ this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none";
+ },
+ downloadClick: () => {
+ const { downloadPopup, downloadButton } = this;
+ const rect = downloadButton.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const buttonCenter = rect.left + rect.width / 2;
+ const isLeftOfCenter = buttonCenter < viewportWidth / 2;
+ if (isLeftOfCenter) {
+ downloadPopup.style.left = `${rect.left + window.scrollX}px`;
+ downloadPopup.style.right = "";
+ } else {
+ downloadPopup.offsetWidth;
+ downloadPopup.style.right = `${viewportWidth - rect.right - window.scrollX}px`;
+ downloadPopup.style.left = "";
}
- };
- setTimeout(() => document.addEventListener("mousedown", hidePopup), 0);
- });
- (_d = this.downloadPopup.querySelector("#download-md")) == null ? void 0 : _d.addEventListener("click", () => {
- this.downloadPopup.style.display = "none";
- const textarea = this.options.textarea;
- if (!textarea)
- return;
- const content = textarea.value;
- const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.md`;
- this.triggerDownload(content, filename, "text/markdown");
- });
- (_e = this.downloadPopup.querySelector("#download-html")) == null ? void 0 : _e.addEventListener("click", () => {
- this.downloadPopup.style.display = "none";
- const textarea = this.options.textarea;
- if (!textarea)
- return;
- const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.html`;
- const html2 = this.htmlWrapper();
- this.triggerDownload(html2, filename, "text/html");
- });
+ downloadPopup.style.top = `${rect.bottom + window.scrollY + 4}px`;
+ downloadPopup.style.display = "block";
+ const hidePopup = (evt) => {
+ if (!downloadPopup.contains(evt.target) && evt.target !== downloadButton) {
+ downloadPopup.style.display = "none";
+ document.removeEventListener("mousedown", hidePopup);
+ }
+ };
+ setTimeout(() => document.addEventListener("mousedown", hidePopup), 0);
+ },
+ restartDisplay: this.options.restartButton ? "" : "none",
+ tweakDisplay: this.options.tweakButton ? "" : "none",
+ downloadDisplay: this.options.downloadButton ? "" : "none",
+ downloadSource: () => {
+ this.downloadPopup.style.display = "none";
+ const textarea = this.options.textarea;
+ if (!textarea)
+ return;
+ const content = textarea.value;
+ const extension = this.mode === "json" ? ".idoc.json" : ".idoc.md";
+ const mimeType = this.mode === "json" ? "application/json" : "text/markdown";
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}${extension}`;
+ this.triggerDownload(content, filename, mimeType);
+ },
+ downloadHtml: () => {
+ this.downloadPopup.style.display = "none";
+ const textarea = this.options.textarea;
+ if (!textarea)
+ return;
+ const html = this.htmlWrapper();
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.html`;
+ this.triggerDownload(html, filename, "text/html");
+ }
+ };
+ this.render();
}
htmlWrapper() {
if (this.mode === "markdown") {
@@ -3254,6 +3400,15 @@ ${htmlJsonJs}
return index$1.htmlJsonWrapper(this.filename, this.options.textarea.value);
}
}
+ addChildren(children) {
+ this.props.children = children;
+ this.render();
+ }
+ render() {
+ mount(ToolbarElement(this.props), this.toolbarElement);
+ this.downloadButton = this.toolbarElement.querySelector("#download");
+ this.downloadPopup = this.toolbarElement.querySelector("#downloadPopup");
+ }
// Helper method to trigger a download
triggerDownload(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
@@ -3269,13 +3424,16 @@ ${htmlJsonJs}
}, 0);
}
showTweakButton() {
- this.tweakButton.style.display = "";
+ this.props.tweakDisplay = "";
+ this.render();
}
showRestartButton() {
- this.restartButton.style.display = "";
+ this.props.restartDisplay = "";
+ this.render();
}
showDownloadButton() {
- this.downloadButton.style.display = "";
+ this.props.downloadDisplay = "";
+ this.render();
}
manageTextareaVisibilityForAgents() {
const { textarea } = this.options;
diff --git a/docs/dist/v1/chartifact.toolbar.umd.js b/docs/dist/v1/chartifact.toolbar.umd.js
index 6a2d243f..fd4aafae 100644
--- a/docs/dist/v1/chartifact.toolbar.umd.js
+++ b/docs/dist/v1/chartifact.toolbar.umd.js
@@ -172,109 +172,224 @@ ${htmlJsonJs}
htmlMarkdownWrapper,
htmlJsonWrapper
};
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+ function decamelize(str, separator = "-") {
+ return str.replace(/([a-z\d])([A-Z])/g, "$1" + separator + "$2").replace(/([A-Z]+)([A-Z][a-z\d]+)/g, "$1" + separator + "$2").toLowerCase();
+ }
+ function createElement(tag, attrs, ...children) {
+ if (typeof tag === "function") {
+ const fn = tag;
+ let props = attrs;
+ if (props === null || props === void 0) {
+ props = { children };
+ } else {
+ props.children = children;
+ }
+ return fn(props);
+ } else {
+ const ns = tag === "svg" ? SVG_NAMESPACE : null;
+ const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
+ const map = attrs;
+ let ref;
+ for (let name in map) {
+ if (name && map.hasOwnProperty(name)) {
+ let value = map[name];
+ if (name === "className" && value !== void 0) {
+ setAttribute(el, ns, "class", value.toString());
+ } else if (name === "disabled" && !value) ;
+ else if (value === null || value === void 0) {
+ continue;
+ } else if (value === true) {
+ setAttribute(el, ns, name, name);
+ } else if (typeof value === "function") {
+ if (name === "ref") {
+ ref = value;
+ } else {
+ el[name.toLowerCase()] = value;
+ }
+ } else if (typeof value === "object") {
+ setAttribute(el, ns, name, flatten(value));
+ } else {
+ setAttribute(el, ns, name, value.toString());
+ }
+ }
+ }
+ if (children && children.length > 0) {
+ appendChildren(el, children);
+ }
+ if (ref) {
+ ref(el);
+ }
+ return el;
+ }
+ }
+ function setAttribute(el, ns, name, value) {
+ if (ns) {
+ el.setAttributeNS(null, name, value);
+ } else {
+ el.setAttribute(name, value);
+ }
+ }
+ function flatten(o) {
+ const arr = [];
+ for (let prop in o)
+ arr.push(`${decamelize(prop, "-")}:${o[prop]}`);
+ return arr.join(";");
+ }
+ function isInsideForeignObject(element) {
+ let current = element;
+ while (current) {
+ if (current.tagName.toLowerCase() === "foreignobject") {
+ return true;
+ }
+ current = current.parentElement;
+ }
+ return false;
+ }
+ function recreateWithSvgNamespace(element) {
+ const svgElement = document.createElementNS(SVG_NAMESPACE, element.tagName.toLowerCase());
+ for (let i = 0; i < element.attributes.length; i++) {
+ const attr = element.attributes[i];
+ svgElement.setAttributeNS(null, attr.name, attr.value);
+ }
+ const eventProperties = [
+ "onclick",
+ "onmousedown",
+ "onmouseup",
+ "onmouseover",
+ "onmouseout",
+ "onmousemove",
+ "onkeydown",
+ "onkeyup",
+ "onkeypress",
+ "onfocus",
+ "onblur"
+ ];
+ for (const prop of eventProperties) {
+ if (element[prop]) {
+ svgElement[prop] = element[prop];
+ }
+ }
+ for (let i = 0; i < element.childNodes.length; i++) {
+ const child = element.childNodes[i];
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ svgElement.appendChild(recreateWithSvgNamespace(child));
+ } else {
+ svgElement.appendChild(child.cloneNode(true));
+ }
+ }
+ return svgElement;
+ }
+ function addChild(parentElement, child) {
+ if (child === null || child === void 0 || typeof child === "boolean") {
+ return;
+ } else if (Array.isArray(child)) {
+ appendChildren(parentElement, child);
+ } else if (isElement(child)) {
+ const childEl = child;
+ if (parentElement.namespaceURI === SVG_NAMESPACE && childEl.namespaceURI !== SVG_NAMESPACE && childEl.tagName.toLowerCase() !== "foreignobject" && !isInsideForeignObject(parentElement)) {
+ const recreated = recreateWithSvgNamespace(childEl);
+ parentElement.appendChild(recreated);
+ } else {
+ parentElement.appendChild(childEl);
+ }
+ } else {
+ parentElement.appendChild(document.createTextNode(child.toString()));
+ }
+ }
+ function appendChildren(parentElement, children) {
+ children.forEach((child) => addChild(parentElement, child));
+ }
+ function isElement(el) {
+ return !!el.nodeType;
+ }
+ function mount(element, container) {
+ container.innerHTML = "";
+ if (element) {
+ addChild(container, element);
+ }
+ }
+ const ToolbarElement = (props) => {
+ const { mode, restartClick, tweakClick, downloadClick, restartDisplay, tweakDisplay, downloadDisplay, downloadSource, downloadHtml, children } = props;
+ const { home, target } = window.location.hostname === "localhost" ? { home: "/", target: "_self" } : { home: "https://microsoft.github.io/", target: "_blank" };
+ const displayMode = mode === "json" ? "json" : "markdown";
+ return /* @__PURE__ */ createElement("div", { className: "toolbar-group", style: { backgroundColor: "inherit" } }, /* @__PURE__ */ createElement("div", { className: "toolbar-item" }, /* @__PURE__ */ createElement("a", { href: `${home}chartifact/`, target }, "Chartifact"), " viewer"), /* @__PURE__ */ createElement("div", { className: "toolbar-item", style: { display: children ? "" : "none" } }, children), /* @__PURE__ */ createElement("div", { className: "toolbar-item" }, /* @__PURE__ */ createElement("button", { type: "button", id: "restart", style: { display: restartDisplay }, onClick: restartClick }, "start over"), /* @__PURE__ */ createElement("button", { type: "button", id: "tweak", style: { display: tweakDisplay }, onClick: tweakClick }, "view source"), /* @__PURE__ */ createElement("button", { type: "button", id: "download", style: { display: downloadDisplay }, onClick: downloadClick }, "download")), /* @__PURE__ */ createElement("div", { id: "downloadPopup", style: {
+ position: "absolute",
+ display: "none",
+ padding: "12px 16px",
+ zIndex: 1,
+ backgroundColor: "inherit"
+ } }, /* @__PURE__ */ createElement("div", { style: { marginBottom: "8px" } }, "Download as:"), /* @__PURE__ */ createElement("ul", null, /* @__PURE__ */ createElement("li", null, "Source (just the ", displayMode, " content)", /* @__PURE__ */ createElement("br", null), /* @__PURE__ */ createElement("button", { type: "button", id: "download-md", style: { marginRight: "8px" }, onClick: downloadSource }, "Source")), /* @__PURE__ */ createElement("li", null, "HTML wrapper (content plus a shareable viewer)", /* @__PURE__ */ createElement("br", null), /* @__PURE__ */ createElement("button", { type: "button", id: "download-html", onClick: downloadHtml }, "HTML wrapper")))));
+ };
class Toolbar {
constructor(toolbarElementOrSelector, options = {}) {
__publicField(this, "toolbarElement");
- __publicField(this, "folderSpan");
- __publicField(this, "tweakButton");
- __publicField(this, "restartButton");
__publicField(this, "downloadButton");
+ __publicField(this, "downloadPopup");
__publicField(this, "mode");
__publicField(this, "filename");
- __publicField(this, "downloadPopup");
- var _a, _b, _c, _d, _e;
+ __publicField(this, "props");
this.options = options;
this.filename = options.filename || "sample";
- this.mode = options.mode || "markdown";
+ const allowedModes = ["markdown", "json"];
+ this.mode = allowedModes.includes(options.mode) ? options.mode : "markdown";
this.toolbarElement = typeof toolbarElementOrSelector === "string" ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
if (!this.toolbarElement) {
throw new Error("Toolbar element not found");
}
- const { home, target } = window.location.hostname === "localhost" ? { home: "/", target: "_self" } : { home: "https://microsoft.github.io/", target: "_blank" };
- const html = `
-
-
-
-
-
-
-
-
- `;
- this.toolbarElement.innerHTML = html;
- this.folderSpan = this.toolbarElement.querySelector("#folderSpan");
- this.tweakButton = this.toolbarElement.querySelector("#tweak");
- this.restartButton = this.toolbarElement.querySelector("#restart");
- this.downloadButton = this.toolbarElement.querySelector("#download");
- this.downloadPopup = this.toolbarElement.querySelector("#downloadPopup");
- if (this.options.tweakButton) {
- this.showTweakButton();
- }
- if (this.options.restartButton) {
- this.showRestartButton();
- }
- if (this.options.downloadButton) {
- this.showDownloadButton();
- }
- (_a = this.tweakButton) == null ? void 0 : _a.addEventListener("click", () => {
- this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none";
- });
- (_b = this.restartButton) == null ? void 0 : _b.addEventListener("click", () => {
- window.location.reload();
- });
- (_c = this.downloadButton) == null ? void 0 : _c.addEventListener("click", (e) => {
- const rect = this.downloadButton.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const buttonCenter = rect.left + rect.width / 2;
- const isLeftOfCenter = buttonCenter < viewportWidth / 2;
- if (isLeftOfCenter) {
- this.downloadPopup.style.left = `${rect.left + window.scrollX}px`;
- this.downloadPopup.style.right = "";
- } else {
- this.downloadPopup.offsetWidth;
- this.downloadPopup.style.right = `${viewportWidth - rect.right - window.scrollX}px`;
- this.downloadPopup.style.left = "";
- }
- this.downloadPopup.style.top = `${rect.bottom + window.scrollY + 4}px`;
- this.downloadPopup.style.display = "block";
- const hidePopup = (evt) => {
- if (!this.downloadPopup.contains(evt.target) && evt.target !== this.downloadButton) {
- this.downloadPopup.style.display = "none";
- document.removeEventListener("mousedown", hidePopup);
+ this.props = {
+ mode: this.mode,
+ restartClick: () => window.location.reload(),
+ tweakClick: () => {
+ this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none";
+ },
+ downloadClick: () => {
+ const { downloadPopup, downloadButton } = this;
+ const rect = downloadButton.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const buttonCenter = rect.left + rect.width / 2;
+ const isLeftOfCenter = buttonCenter < viewportWidth / 2;
+ if (isLeftOfCenter) {
+ downloadPopup.style.left = `${rect.left + window.scrollX}px`;
+ downloadPopup.style.right = "";
+ } else {
+ downloadPopup.offsetWidth;
+ downloadPopup.style.right = `${viewportWidth - rect.right - window.scrollX}px`;
+ downloadPopup.style.left = "";
}
- };
- setTimeout(() => document.addEventListener("mousedown", hidePopup), 0);
- });
- (_d = this.downloadPopup.querySelector("#download-md")) == null ? void 0 : _d.addEventListener("click", () => {
- this.downloadPopup.style.display = "none";
- const textarea = this.options.textarea;
- if (!textarea) return;
- const content = textarea.value;
- const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.md`;
- this.triggerDownload(content, filename, "text/markdown");
- });
- (_e = this.downloadPopup.querySelector("#download-html")) == null ? void 0 : _e.addEventListener("click", () => {
- this.downloadPopup.style.display = "none";
- const textarea = this.options.textarea;
- if (!textarea) return;
- const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.html`;
- const html2 = this.htmlWrapper();
- this.triggerDownload(html2, filename, "text/html");
- });
+ downloadPopup.style.top = `${rect.bottom + window.scrollY + 4}px`;
+ downloadPopup.style.display = "block";
+ const hidePopup = (evt) => {
+ if (!downloadPopup.contains(evt.target) && evt.target !== downloadButton) {
+ downloadPopup.style.display = "none";
+ document.removeEventListener("mousedown", hidePopup);
+ }
+ };
+ setTimeout(() => document.addEventListener("mousedown", hidePopup), 0);
+ },
+ restartDisplay: this.options.restartButton ? "" : "none",
+ tweakDisplay: this.options.tweakButton ? "" : "none",
+ downloadDisplay: this.options.downloadButton ? "" : "none",
+ downloadSource: () => {
+ this.downloadPopup.style.display = "none";
+ const textarea = this.options.textarea;
+ if (!textarea) return;
+ const content = textarea.value;
+ const extension = this.mode === "json" ? ".idoc.json" : ".idoc.md";
+ const mimeType = this.mode === "json" ? "application/json" : "text/markdown";
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}${extension}`;
+ this.triggerDownload(content, filename, mimeType);
+ },
+ downloadHtml: () => {
+ this.downloadPopup.style.display = "none";
+ const textarea = this.options.textarea;
+ if (!textarea) return;
+ const html = this.htmlWrapper();
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.html`;
+ this.triggerDownload(html, filename, "text/html");
+ }
+ };
+ this.render();
}
htmlWrapper() {
if (this.mode === "markdown") {
@@ -283,6 +398,15 @@ ${htmlJsonJs}
return index$1.htmlJsonWrapper(this.filename, this.options.textarea.value);
}
}
+ addChildren(children) {
+ this.props.children = children;
+ this.render();
+ }
+ render() {
+ mount(ToolbarElement(this.props), this.toolbarElement);
+ this.downloadButton = this.toolbarElement.querySelector("#download");
+ this.downloadPopup = this.toolbarElement.querySelector("#downloadPopup");
+ }
// Helper method to trigger a download
triggerDownload(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
@@ -298,13 +422,16 @@ ${htmlJsonJs}
}, 0);
}
showTweakButton() {
- this.tweakButton.style.display = "";
+ this.props.tweakDisplay = "";
+ this.render();
}
showRestartButton() {
- this.restartButton.style.display = "";
+ this.props.restartDisplay = "";
+ this.render();
}
showDownloadButton() {
- this.downloadButton.style.display = "";
+ this.props.downloadDisplay = "";
+ this.render();
}
manageTextareaVisibilityForAgents() {
const { textarea } = this.options;
diff --git a/package-lock.json b/package-lock.json
index d8e19adc..94dc39b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,6 +47,7 @@
"rimraf": "^6.0.1",
"rollup-plugin-dts": "^6.2.1",
"ts-json-schema-generator": "^2.4.0",
+ "tsx-create-element": "^2.3.0",
"typescript": "^5.8.3",
"vega-typings": "^2.0.1",
"vite": "^6.3.6"
@@ -13761,6 +13762,16 @@
"node": ">=0.6.x"
}
},
+ "node_modules/tsx-create-element": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tsx-create-element/-/tsx-create-element-2.3.0.tgz",
+ "integrity": "sha512-BMRK11fA8dQSqufL7u5HpqVSIzHBrXJldT3VNKJ1677fJ9NwEWEL7vRfA1ue6rtRnkKA99dwiLk1TR6Ox+erJw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
diff --git a/package.json b/package.json
index 4edaabb8..60b438c2 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"rimraf": "^6.0.1",
"rollup-plugin-dts": "^6.2.1",
"ts-json-schema-generator": "^2.4.0",
+ "tsx-create-element": "^2.3.0",
"typescript": "^5.8.3",
"vega-typings": "^2.0.1",
"vite": "^6.3.6"
diff --git a/packages/host/src/folder.ts b/packages/host/src/folder.tsx
similarity index 55%
rename from packages/host/src/folder.ts
rename to packages/host/src/folder.tsx
index 0fb58b5a..4cc63961 100644
--- a/packages/host/src/folder.ts
+++ b/packages/host/src/folder.tsx
@@ -3,9 +3,48 @@
* Licensed under the MIT License.
*/
-import { Folder } from "@microsoft/chartifact-schema-folder";
+import { Folder, DocRef } from "@microsoft/chartifact-schema-folder";
import { Listener } from "./listener.js";
import { loadViaUrl } from "./url.js";
+import { createElement } from 'tsx-create-element';
+
+interface FolderDisplayProps {
+ title: string;
+ docIndex: number;
+ docs: DocRef[];
+ prevClick: () => void;
+ nextClick: () => void;
+ pageSelectChange: (index: number) => void;
+}
+
+const FolderDisplay = (props: FolderDisplayProps) => {
+ const { title, docIndex, docs, prevClick, nextClick } = props;
+ return (
+
+ {title}
+
+ (document {docIndex + 1} of {docs.length})
+
+
+
+
+
+
+ );
+}
export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
if (!folder || !folder.docs) {
@@ -34,35 +73,6 @@ export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
let docIndex = 0;
- const { folderSpan } = host.toolbar;
- folderSpan.style.display = '';
-
- // Create Previous and Next buttons
- const prevBtn = document.createElement('button');
- prevBtn.textContent = 'Previous';
- prevBtn.disabled = docIndex === 0;
-
- const nextBtn = document.createElement('button');
- nextBtn.textContent = 'Next';
- nextBtn.disabled = docIndex === folder.docs.length - 1;
-
- // Create dropdown for page selection
- const pageSelect = document.createElement('select');
- for (let i = 0; i < folder.docs.length; i++) {
- const option = document.createElement('option');
- option.value = (i + 1).toString();
- option.textContent = folder.docs[i].title ? folder.docs[i].title : `Page ${i + 1}`;
- pageSelect.appendChild(option);
- }
- pageSelect.value = (docIndex + 1).toString();
-
- // --- Wrap controls in a div ---
- const navDiv = document.createElement('div');
- navDiv.style.display = 'inline-block';
- navDiv.appendChild(prevBtn);
- navDiv.appendChild(pageSelect);
- navDiv.appendChild(nextBtn);
-
function getHashParam(key: string): string | undefined {
const params = new URLSearchParams(window.location.hash.slice(1));
return params.get(key) ?? undefined;
@@ -74,64 +84,44 @@ export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
window.location.hash = params.toString();
}
- function updateFolderTitle() {
- // Clear previous content
- folderSpan.innerHTML = '';
-
- // Folder label
- const label = document.createElement('span');
- label.textContent = `${folder.title} `;
-
- // Document count in its own inline-block div
- const docCountDiv = document.createElement('div');
- docCountDiv.style.display = 'inline-block';
- docCountDiv.style.marginRight = '0.5em';
- docCountDiv.textContent = `(document ${docIndex + 1} of ${folder.docs.length}) `;
-
- folderSpan.appendChild(label);
- folderSpan.appendChild(docCountDiv);
- folderSpan.appendChild(navDiv);
- }
-
- // Initial folder title
- updateFolderTitle();
+ const props: FolderDisplayProps = {
+ title: folder.title,
+ docIndex,
+ docs: folder.docs,
+ prevClick: () => {
+ if (docIndex > 0) {
+ updatePage(docIndex - 1, true);
+ }
+ },
+ nextClick: () => {
+ if (docIndex < folder.docs.length - 1) {
+ updatePage(docIndex + 1, true);
+ }
+ },
+ pageSelectChange: (index) => {
+ if (index >= 0 && index <= folder.docs.length - 1) {
+ updatePage(index, true);
+ }
+ }
+ };
function updatePage(newDocIndex: number, setHash: boolean = false) {
docIndex = newDocIndex;
if (setHash) {
setHashParam('page', docIndex + 1);
}
- prevBtn.disabled = docIndex === 0;
- nextBtn.disabled = docIndex === folder.docs.length - 1;
- pageSelect.value = (docIndex + 1).toString();
- // Use the function to update the folder title
- updateFolderTitle();
+ host.toolbar.addChildren();
+
+ const pageSelect = document.querySelector('#pageSelect') as HTMLSelectElement;
+ if (pageSelect) {
+ pageSelect.value = (docIndex + 1).toString();
+ }
const title = folder.docs[docIndex].title || `Page ${docIndex + 1}`;
resolveUrl(title, folderUrl, folder.docs[docIndex].href, host);
}
- pageSelect.onchange = () => {
- const selectedPage = parseInt(pageSelect.value, 10);
- if (selectedPage >= 1 && selectedPage <= folder.docs.length) {
- updatePage(selectedPage - 1, true);
- }
- };
-
- // Button click handlers
- prevBtn.onclick = () => {
- if (docIndex > 0) {
- updatePage(docIndex - 1, true);
- }
- };
-
- nextBtn.onclick = () => {
- if (docIndex < folder.docs.length - 1) {
- updatePage(docIndex + 1, true);
- }
- };
-
// Set initial hash and handle hash navigation
function goToPageFromHash() {
const pageStr = getHashParam('page');
@@ -153,9 +143,6 @@ export function loadFolder(folderUrl: string, folder: Folder, host: Listener) {
}
goToPageFromHash();
- // Add buttons and dropdown to the toolbar
- folderSpan.appendChild(navDiv);
-
//resolveUrl(folderUrl, folder.docUrls[docIndex], host);
}
diff --git a/packages/host/tsconfig.json b/packages/host/tsconfig.json
index 4b1e9828..cdb23210 100644
--- a/packages/host/tsconfig.json
+++ b/packages/host/tsconfig.json
@@ -1,6 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
+ "jsx": "react",
+ "jsxFactory": "createElement",
"outDir": "dist/esnext"
},
"include": [
diff --git a/packages/toolbar/chartifact-toolbar.css b/packages/toolbar/chartifact-toolbar.css
index 2e4e8848..402c025c 100644
--- a/packages/toolbar/chartifact-toolbar.css
+++ b/packages/toolbar/chartifact-toolbar.css
@@ -58,17 +58,17 @@
}
/* Default: stack toolbar sections vertically */
-.chartifact-toolbar > div {
+.chartifact-toolbar .toolbar-item {
margin: 6px 0;
}
/* Wide screens: layout horizontally */
@media (min-width: 1000px) {
- .chartifact-toolbar {
+ .chartifact-toolbar .toolbar-group {
display: flex;
justify-content: space-between;
}
- .chartifact-toolbar > div {
+ .chartifact-toolbar .toolbar-item {
display: inline-block;
margin: 0;
}
diff --git a/packages/toolbar/src/toolbar.ts b/packages/toolbar/src/toolbar.ts
deleted file mode 100644
index 29dbc627..00000000
--- a/packages/toolbar/src/toolbar.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-/**
-* Copyright (c) Microsoft Corporation.
-* Licensed under the MIT License.
-*/
-
-import * as hw from 'html-wrapper';
-
-export interface ToolbarOptions {
- tweakButton?: boolean;
- downloadButton?: boolean;
- restartButton?: boolean;
- textarea?: HTMLTextAreaElement;
- mode?: 'markdown' | 'json';
- filename?: string;
-}
-
-export class Toolbar {
- public toolbarElement: HTMLElement;
- public folderSpan: HTMLElement;
- public tweakButton: HTMLButtonElement;
- public restartButton: HTMLButtonElement;
- public downloadButton: HTMLButtonElement;
- public mode: 'markdown' | 'json';
- public filename: string;
- private downloadPopup: HTMLDivElement;
-
- constructor(toolbarElementOrSelector: HTMLElement | string, public options: ToolbarOptions = {}) {
- this.filename = options.filename || 'sample';
- this.mode = options.mode || 'markdown';
- this.toolbarElement = typeof toolbarElementOrSelector === 'string' ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
-
- if (!this.toolbarElement) {
- throw new Error('Toolbar element not found');
- }
-
- const { home, target } = (window.location.hostname === 'localhost')
- ? { home: '/', target: '_self' }
- : { home: 'https://microsoft.github.io/', target: '_blank' };
-
- const html = `
-
-
-
-
-
-
-
-
- `;
-
- this.toolbarElement.innerHTML = html;
-
- this.folderSpan = this.toolbarElement.querySelector('#folderSpan') as HTMLElement;
- this.tweakButton = this.toolbarElement.querySelector('#tweak') as HTMLButtonElement;
- this.restartButton = this.toolbarElement.querySelector('#restart') as HTMLButtonElement;
- this.downloadButton = this.toolbarElement.querySelector('#download') as HTMLButtonElement;
- this.downloadPopup = this.toolbarElement.querySelector('#downloadPopup') as HTMLDivElement;
-
- if (this.options.tweakButton) {
- this.showTweakButton();
- }
-
- if (this.options.restartButton) {
- this.showRestartButton();
- }
-
- if (this.options.downloadButton) {
- this.showDownloadButton();
- }
-
- this.tweakButton?.addEventListener('click', () => {
- this.options.textarea.style.display = this.options.textarea.style.display === 'none' ? '' : 'none';
- });
-
- this.restartButton?.addEventListener('click', () => {
- window.location.reload();
- });
-
- this.downloadButton?.addEventListener('click', (e) => {
- // Position popup near the button
- const rect = this.downloadButton.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const buttonCenter = rect.left + rect.width / 2;
- const isLeftOfCenter = buttonCenter < viewportWidth / 2;
-
- if (isLeftOfCenter) {
- // Align popup to the left of the button
- this.downloadPopup.style.left = `${rect.left + window.scrollX}px`;
- this.downloadPopup.style.right = ''; // Clear right alignment
- } else {
- // Align popup to the right of the button
- const popupWidth = this.downloadPopup.offsetWidth;
- this.downloadPopup.style.right = `${viewportWidth - rect.right - window.scrollX}px`;
- this.downloadPopup.style.left = ''; // Clear left alignment
- }
-
- this.downloadPopup.style.top = `${rect.bottom + window.scrollY + 4}px`;
- this.downloadPopup.style.display = 'block';
-
- // Hide popup on outside click
- const hidePopup = (evt: MouseEvent) => {
- if (!this.downloadPopup.contains(evt.target as Node) && evt.target !== this.downloadButton) {
- this.downloadPopup.style.display = 'none';
- document.removeEventListener('mousedown', hidePopup);
- }
- };
- setTimeout(() => document.addEventListener('mousedown', hidePopup), 0);
- });
-
- // Download as markdown
- this.downloadPopup.querySelector('#download-md')?.addEventListener('click', () => {
- this.downloadPopup.style.display = 'none';
- const textarea = this.options.textarea;
- if (!textarea) return;
- const content = textarea.value;
- const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.md`;
- this.triggerDownload(content, filename, 'text/markdown');
- });
-
- // Download as HTML wrapper
- this.downloadPopup.querySelector('#download-html')?.addEventListener('click', () => {
- this.downloadPopup.style.display = 'none';
- const textarea = this.options.textarea;
- if (!textarea) return;
- const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.html`;
- const html = this.htmlWrapper();
- this.triggerDownload(html, filename, 'text/html');
- });
- }
-
- public htmlWrapper() {
- if (this.mode === 'markdown') {
- return hw.default.htmlMarkdownWrapper(this.filename, this.options.textarea.value);
- } else if (this.mode === 'json') {
- return hw.default.htmlJsonWrapper(this.filename, this.options.textarea.value);
- }
- }
-
- // Helper method to trigger a download
- private triggerDownload(content: string, filename: string, mimeType: string) {
- const blob = new Blob([content], { type: mimeType });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }, 0);
- }
-
- showTweakButton() {
- this.tweakButton.style.display = '';
- }
-
- showRestartButton() {
- this.restartButton.style.display = '';
- }
-
- showDownloadButton() {
- this.downloadButton.style.display = '';
- }
-
- manageTextareaVisibilityForAgents() {
- const { textarea } = this.options;
-
- if (!textarea) {
- throw new Error('Textarea element not found');
- }
-
- // Bot-friendly content hiding strategy:
- // 1. Bots that don't execute JS will see the textarea content in full
- // 2. Bots that execute JS but have short timeouts will see content for 300ms
- // 3. Human users see no visual flash due to flex:0 + removed padding/border
- textarea.style.flex = '0'; // Collapse to zero width, no visual space
- textarea.style.padding = '0'; // Remove default padding that creates pixel shift
- textarea.style.border = '0'; // Remove default border that creates pixel shift
- setTimeout(() => {
- // After 300ms (longer than most bot JS timeouts), restore defaults and hide
- textarea.style.flex = ''; // Restore original flex value from CSS
- textarea.style.padding = ''; // Restore original padding from CSS
- textarea.style.border = ''; // Restore original border from CSS
- textarea.style.display = 'none'; // Fully hide from users (but they can unhide to edit)
- }, 300);
-
- }
-}
-
-function filenameWithoutPathOrExtension(filename: string) {
- // Remove everything before the last slash or backslash
- const base = filename.split(/[\\/]/).pop() || filename;
- // If .idoc appears, remove it and everything after
- const idocIdx = base.indexOf('.idoc');
- if (idocIdx !== -1) {
- return base.substring(0, idocIdx);
- }
- // Otherwise, remove the last extension if present
- const lastDot = base.lastIndexOf('.');
- if (lastDot > 0) {
- return base.substring(0, lastDot);
- }
- return base;
-}
-
-// --- TESTS for filenameWithoutPathOrExtension ---
-
-// const filenameTests: [string, string][] = [
-// // [input, expected]
-// ["foo.md", "foo"],
-// ["foo.idoc.md", "foo"],
-// ["foo.idoc.json", "foo"],
-// ["foo", "foo"],
-// ["foo.bar.baz.md", "foo.bar.baz"],
-// ["C:\\folder\\foo.md", "foo"],
-// ["/home/user/foo.md", "foo"],
-// ["folder/foo.idoc.md", "foo"],
-// ["folder\\foo.idoc.json", "foo"],
-// ["folder.with.dots/foo.bar.baz.md", "foo.bar.baz"],
-// ["folder.with.dots\\foo.bar.baz.md", "foo.bar.baz"],
-// ["", ""],
-// ];
-
-// filenameTests.forEach(([input, expected], i) => {
-// const result = filenameWithoutPathOrExtension(input);
-// const pass = result === expected;
-// // eslint-disable-next-line no-console
-// console.log(
-// `${pass ? '✅' : '❌'} Test ${i + 1}: ${pass ? 'PASS' : `FAIL\n Input: ${input}\n Got: ${result}\n Expected: ${expected}`}`
-// );
-// });
-
diff --git a/packages/toolbar/src/toolbar.tsx b/packages/toolbar/src/toolbar.tsx
new file mode 100644
index 00000000..2ff4c9b5
--- /dev/null
+++ b/packages/toolbar/src/toolbar.tsx
@@ -0,0 +1,283 @@
+/**
+* Copyright (c) Microsoft Corporation.
+* Licensed under the MIT License.
+*/
+
+import * as hw from 'html-wrapper';
+import { createElement, mount } from 'tsx-create-element';
+
+export interface ToolbarOptions {
+ tweakButton?: boolean;
+ downloadButton?: boolean;
+ restartButton?: boolean;
+ textarea?: HTMLTextAreaElement;
+ /**
+ * Mode to display content, allowed values: 'markdown' | 'json'.
+ * Only trusted values should be supplied.
+ * If an untrusted value is provided, it will be ignored and replaced with 'markdown'.
+ */
+ mode?: 'markdown' | 'json';
+ filename?: string;
+}
+
+interface ToolbarProps {
+ mode: 'markdown' | 'json';
+ restartClick: () => void;
+ tweakClick: () => void;
+ downloadClick: () => void;
+ restartDisplay: 'none' | '';
+ tweakDisplay: 'none' | '';
+ downloadDisplay: 'none' | '';
+ downloadSource: () => void;
+ downloadHtml: () => void;
+ children?: JSX.Element;
+}
+
+const ToolbarElement = (props: ToolbarProps) => {
+
+ const { mode, restartClick, tweakClick, downloadClick, restartDisplay, tweakDisplay, downloadDisplay, downloadSource, downloadHtml, children } = props;
+
+ const { home, target } = (window.location.hostname === 'localhost')
+ ? { home: '/', target: '_self' }
+ : { home: 'https://microsoft.github.io/', target: '_blank' };
+
+ const displayMode = mode === 'json' ? 'json' : 'markdown';
+
+ return (
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+ );
+}
+
+export class Toolbar {
+ public toolbarElement: HTMLElement;
+ public downloadButton: HTMLButtonElement;
+ public downloadPopup: HTMLDivElement;
+ public mode: 'markdown' | 'json';
+ public filename: string;
+ public props: ToolbarProps;
+
+ constructor(toolbarElementOrSelector: HTMLElement | string, public options: ToolbarOptions = {}) {
+ this.filename = options.filename || 'sample';
+ // Runtime check to restrict mode to allowed values only
+ const allowedModes = ['markdown', 'json'];
+ this.mode = allowedModes.includes(options.mode as string) ? options.mode as 'markdown' | 'json' : 'markdown';
+
+ this.toolbarElement = typeof toolbarElementOrSelector === 'string' ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector;
+
+ if (!this.toolbarElement) {
+ throw new Error('Toolbar element not found');
+ }
+
+ this.props = {
+ mode: this.mode,
+ restartClick: () => window.location.reload(),
+ tweakClick: () => {
+ this.options.textarea.style.display = this.options.textarea.style.display === 'none' ? '' : 'none';
+ },
+ downloadClick: () => {
+ const { downloadPopup, downloadButton } = this;
+
+ // Position popup near the button
+ const rect = downloadButton.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const buttonCenter = rect.left + rect.width / 2;
+ const isLeftOfCenter = buttonCenter < viewportWidth / 2;
+
+ if (isLeftOfCenter) {
+ // Align popup to the left of the button
+ downloadPopup.style.left = `${rect.left + window.scrollX}px`;
+ downloadPopup.style.right = ''; // Clear right alignment
+ } else {
+ // Align popup to the right of the button
+ const popupWidth = downloadPopup.offsetWidth;
+ downloadPopup.style.right = `${viewportWidth - rect.right - window.scrollX}px`;
+ downloadPopup.style.left = ''; // Clear left alignment
+ }
+
+ downloadPopup.style.top = `${rect.bottom + window.scrollY + 4}px`;
+ downloadPopup.style.display = 'block';
+
+ // Hide popup on outside click
+ const hidePopup = (evt: MouseEvent) => {
+ if (!downloadPopup.contains(evt.target as Node) && evt.target !== downloadButton) {
+ downloadPopup.style.display = 'none';
+ document.removeEventListener('mousedown', hidePopup);
+ }
+ };
+ setTimeout(() => document.addEventListener('mousedown', hidePopup), 0);
+ },
+ restartDisplay: this.options.restartButton ? '' : 'none',
+ tweakDisplay: this.options.tweakButton ? '' : 'none',
+ downloadDisplay: this.options.downloadButton ? '' : 'none',
+ downloadSource: () => {
+ this.downloadPopup.style.display = 'none';
+ const textarea = this.options.textarea;
+ if (!textarea) return;
+ const content = textarea.value;
+ const extension = this.mode === 'json' ? '.idoc.json' : '.idoc.md';
+ const mimeType = this.mode === 'json' ? 'application/json' : 'text/markdown';
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}${extension}`;
+ this.triggerDownload(content, filename, mimeType);
+
+ },
+ downloadHtml: () => {
+ this.downloadPopup.style.display = 'none';
+ const textarea = this.options.textarea;
+ if (!textarea) return;
+ const html = this.htmlWrapper();
+ const filename = `${filenameWithoutPathOrExtension(this.filename)}.idoc.html`;
+ this.triggerDownload(html, filename, 'text/html');
+ },
+ };
+
+ this.render();
+ }
+
+ public htmlWrapper() {
+ if (this.mode === 'markdown') {
+ return hw.default.htmlMarkdownWrapper(this.filename, this.options.textarea.value);
+ } else if (this.mode === 'json') {
+ return hw.default.htmlJsonWrapper(this.filename, this.options.textarea.value);
+ }
+ }
+
+ addChildren(children: JSX.Element) {
+ this.props.children = children;
+ this.render();
+ }
+
+ render() {
+ mount(ToolbarElement(this.props), this.toolbarElement);
+ this.downloadButton = this.toolbarElement.querySelector('#download') as HTMLButtonElement;
+ this.downloadPopup = this.toolbarElement.querySelector('#downloadPopup') as HTMLDivElement;
+ }
+
+ // Helper method to trigger a download
+ private triggerDownload(content: string, filename: string, mimeType: string) {
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 0);
+ }
+
+ showTweakButton() {
+ this.props.tweakDisplay = '';
+ this.render();
+ }
+
+ showRestartButton() {
+ this.props.restartDisplay = '';
+ this.render();
+ }
+
+ showDownloadButton() {
+ this.props.downloadDisplay = '';
+ this.render();
+ }
+
+ manageTextareaVisibilityForAgents() {
+ const { textarea } = this.options;
+
+ if (!textarea) {
+ throw new Error('Textarea element not found');
+ }
+
+ // Bot-friendly content hiding strategy:
+ // 1. Bots that don't execute JS will see the textarea content in full
+ // 2. Bots that execute JS but have short timeouts will see content for 300ms
+ // 3. Human users see no visual flash due to flex:0 + removed padding/border
+ textarea.style.flex = '0'; // Collapse to zero width, no visual space
+ textarea.style.padding = '0'; // Remove default padding that creates pixel shift
+ textarea.style.border = '0'; // Remove default border that creates pixel shift
+ setTimeout(() => {
+ // After 300ms (longer than most bot JS timeouts), restore defaults and hide
+ textarea.style.flex = ''; // Restore original flex value from CSS
+ textarea.style.padding = ''; // Restore original padding from CSS
+ textarea.style.border = ''; // Restore original border from CSS
+ textarea.style.display = 'none'; // Fully hide from users (but they can unhide to edit)
+ }, 300);
+
+ }
+}
+
+function filenameWithoutPathOrExtension(filename: string) {
+ // Remove everything before the last slash or backslash
+ const base = filename.split(/[\\/]/).pop() || filename;
+ // If .idoc appears, remove it and everything after
+ const idocIdx = base.indexOf('.idoc');
+ if (idocIdx !== -1) {
+ return base.substring(0, idocIdx);
+ }
+ // Otherwise, remove the last extension if present
+ const lastDot = base.lastIndexOf('.');
+ if (lastDot > 0) {
+ return base.substring(0, lastDot);
+ }
+ return base;
+}
+
+// --- TESTS for filenameWithoutPathOrExtension ---
+
+// const filenameTests: [string, string][] = [
+// // [input, expected]
+// ["foo.md", "foo"],
+// ["foo.idoc.md", "foo"],
+// ["foo.idoc.json", "foo"],
+// ["foo", "foo"],
+// ["foo.bar.baz.md", "foo.bar.baz"],
+// ["C:\\folder\\foo.md", "foo"],
+// ["/home/user/foo.md", "foo"],
+// ["folder/foo.idoc.md", "foo"],
+// ["folder\\foo.idoc.json", "foo"],
+// ["folder.with.dots/foo.bar.baz.md", "foo.bar.baz"],
+// ["folder.with.dots\\foo.bar.baz.md", "foo.bar.baz"],
+// ["", ""],
+// ];
+
+// filenameTests.forEach(([input, expected], i) => {
+// const result = filenameWithoutPathOrExtension(input);
+// const pass = result === expected;
+// // eslint-disable-next-line no-console
+// console.log(
+// `${pass ? '✅' : '❌'} Test ${i + 1}: ${pass ? 'PASS' : `FAIL\n Input: ${input}\n Got: ${result}\n Expected: ${expected}`}`
+// );
+// });
+
diff --git a/packages/toolbar/tsconfig.json b/packages/toolbar/tsconfig.json
index 4b1e9828..cdb23210 100644
--- a/packages/toolbar/tsconfig.json
+++ b/packages/toolbar/tsconfig.json
@@ -1,6 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
+ "jsx": "react",
+ "jsxFactory": "createElement",
"outDir": "dist/esnext"
},
"include": [