Skip to content

Instantly share code, notes, and snippets.

@Hanlin-Dong
Created September 7, 2023 16:20
Show Gist options
  • Save Hanlin-Dong/636da9de2b6de3aaa9ee8cea5fbb4e9d to your computer and use it in GitHub Desktop.
Save Hanlin-Dong/636da9de2b6de3aaa9ee8cea5fbb4e9d to your computer and use it in GitHub Desktop.
Create navigation bar and progress bars in PowerPoint!
name: NavBar-v1.0.2
description: Create navigation bar and progress bars in PowerPoint!
host: POWERPOINT
api_set: {}
script:
content: |
const { createApp } = Vue;
createApp({
data() {
return {
version: "1.0.2",
isSyncing: true,
slideCount: 0,
slideTitles: [],
slideSectionTitles: [],
slideSkipped: [],
slideIds: [],
sectionBar: {
direction: "Horizontal",
distribution: "EqualSpacing",
rangeWidth: 960,
rangeHeight: 30,
rangeLeft: 0,
rangeTop: 0,
activeFormat: defaultActiveFormat(),
inactiveFormat: defaultInactiveFormat(),
showActiveOnly: false
},
cursor: {
replaceWithImage: false,
data: "Rectangle",
width: 200,
height: 10,
leftOffset: 0,
topOffset: 30,
alignment: "Start",
format: defaultActiveFormat(),
pastFormat: defaultInactiveFormat(),
upcomingFormat: defaultInactiveFormat(),
by: "Section",
showInactive: false,
distanceWithinSection: null,
offsetPerp: false,
fullfill: false,
fullfillGap: null,
fitImage: false,
pastImage: {
data: ""
},
upcomingImage: {
data: ""
},
replaceInactiveWithImage: false,
replaceInactiveWithActiveImage: false
}
};
},
methods: {
exec(fun, msg) {
this.isSyncing = true;
fun()
.then(() => {
this.isSyncing = false;
this.save();
this.toastSuccess(msg);
})
.catch((reason) => {
this.isSyncing = false;
this.toastFailure(reason.message);
console.log(reason);
});
},
createNavbar() {
this.exec(async () => {
await createSectionBar(this.sectionBarConfig);
}, "Success! Configurations saved.");
},
deleteAll() {
this.isSyncing = true;
(async () => {
deleteAllNavbarShapes(this.slideCount);
})()
.then(() => {
this.isSyncing = false;
this.toastSuccess("Finished.");
})
.catch((reason) => {
this.isSyncing = false;
this.toastFailure(reason.message);
});
},
createCursor() {
tryCatch(() => {
createCursor(this.cursorConfig, this.sectionBarConfig);
});
},
save() {
saveToTag(this.saveData)
.then(() => {})
.catch((reason) => {
this.toastFailure(reason.message);
});
this.load();
},
readSectionBarRange() {
readSelectedShapeFormat().then((format) => {
this.sectionBar.rangeWidth = format.width;
this.sectionBar.rangeHeight = format.height;
this.sectionBar.rangeLeft = format.left;
this.sectionBar.rangeTop = format.top;
});
},
loadImage(ref, target) {
this.isSyncing = true;
const file = ref.files[0];
if (!file) {
this.toastFailure("Please select a file");
return;
}
const filename = file.name;
const reader = new FileReader();
reader.onloadend = (res) => {
target.data = res.target.result.replace(/^.*base64,/g, "");
this.isSyncing = false;
this.toastSuccess("The selected image is loaded.");
};
reader.onerror = (res) => {
this.isSyncing = false;
this.toasFailure("The selected file cannot be loaded.");
console.log(res);
};
reader.readAsDataURL(file);
},
loadImageCursor() {
this.loadImage(this.$refs.fileImageCursor, this.cursor);
},
loadImagePast() {
this.loadImage(this.$refs.fileImagePast, this.cursor.pastImage);
},
loadImageUpcoming() {
this.loadImage(this.$refs.fileImageUpcoming, this.cursor.upcomingImage);
},
load() {
readPresentationProp()
.then((res) => {
console.log(res);
this.slideTitles = res.slideTitles;
this.slideIds = res.slideIds;
this.slideCount = res.slideCount;
if (Object.keys(res.savedData).length === 0) {
this.slideSectionTitles = Array(res.slideCount).fill("");
this.slideSkipped = Array(res.slideCount).fill(false);
this.initMaterialize();
} else {
let { slideIdToSectionTitle, slideIdSkipped } = res.savedData;
this.slideSectionTitles = res.slideIds.map((id) => {
return slideIdToSectionTitle[id] === undefined ? "" : slideIdToSectionTitle[id];
});
this.slideSkipped = res.slideIds.map((id) => {
return slideIdSkipped[id] === undefined ? false : true;
});
Object.assign(this.sectionBar, res.savedData.sectionBar);
Object.assign(this.cursor, res.savedData.cursor);
this.initMaterialize();
}
this.isSyncing = false;
})
.catch((reason) => {
console.log(reason);
});
},
refresh() {
this.save();
this.load();
},
log() {
createImageCursor(this.cursorConfig, this.sectionBarConfig);
},
toastSuccess(message) {
M.toast({ html: message, classes: "green darken-4", displayLength: 2000 });
},
toastFailure(message) {
M.toast({ html: message, classes: "red darken-4", displayLength: 2000 });
},
matchShapeFormat(which) {
readSelectedShapeFormat().then((format) => {
switch (which) {
case "Active":
this.sectionBar.activeFormat = format;
break;
case "Inactive":
this.sectionBar.inactiveFormat = format;
break;
case "Current":
this.cursor.format = format;
break;
case "Past":
this.cursor.pastFormat = format;
break;
case "Upcoming":
this.cursor.upcomingFormat = format;
break;
default:
throw new Error(`Invalid matching target : ${which} in readSelectedShapeFormat()`);
}
});
},
readCursorProp() {
readSelectedShapeFormat().then((format) => {
this.cursor.width = format.width;
this.cursor.height = format.height;
this.cursor.leftOffset = format.left - this.sectionBar.rangeLeft;
this.cursor.topOffset = format.top - this.sectionBar.rangeTop;
});
},
initMaterialize() {
M.FormSelect.init(document.querySelectorAll("select"));
M.Modal.init(document.querySelectorAll(".modal"));
M.Tooltip.init(document.querySelectorAll(".tooltipped"));
}
},
computed: {
slideIdToSectionTitle() {
let res = {};
this.slideSectionTitles.map((name, i) => {
if (name !== "") {
res[this.slideIds[i]] = name;
}
});
return res;
},
sectionBarConfig() {
return {
...this.sectionBar,
sectionTitles: this.sectionTitles,
slideSectionIds: this.slideSectionIds
};
},
cursorConfig() {
const res = { ...this.cursor };
res.kind = this.cursor.replaceWithImage ? "Image" : "Shape";
if (!res.distanceWithinSection) {
res.distanceWithinSection = 20;
}
if (this.cursor.fullfill) {
if (this.sectionBar.direction === "Horizontal") {
res.width = this.cursor.fullfillGap === null ? 0 : -this.cursor.fullfillGap;
} else if (this.sectionBar.direction === "Vertical") {
res.height = this.cursor.fullfillGap === null ? 0 : -this.cursor.fullfillGap;
}
}
delete res["fullfill"];
delete res["fullfillGap"];
return res;
},
saveData() {
return {
version: this.version,
slideIdToSectionTitle: this.slideIdToSectionTitle,
slideIdSkipped: this.slideIdSkipped,
sectionBar: this.sectionBar,
cursor: this.cursor
};
},
sectionTitles() {
let sectionTitles = [];
let currentsectionTitle = "";
for (let i = 0; i < this.slideCount; i++) {
if (this.slideSectionTitles[i] !== "" && this.slideSectionTitles[i] != currentsectionTitle) {
currentsectionTitle = this.slideSectionTitles[i];
if (currentsectionTitle != "") {
sectionTitles.push(currentsectionTitle);
}
}
}
return sectionTitles;
},
slideSectionIds() {
let currentId = -1;
return Array(this.slideCount)
.fill(0)
.map((_, i) => {
if (this.slideSectionTitles[i] !== "") {
currentId += 1;
}
if (this.slideSkipped[i]) {
return -1;
} else {
return currentId;
}
});
},
slideIdSkipped() {
let res = {};
for (let i = 0; i < this.slideSkipped.length; i++) {
if (this.slideSkipped[i]) {
res[this.slideIds[i]] = true;
}
}
return res;
}
},
mounted() {
this.load();
}
}).mount("#navbar-app");
// For 16:9 only. Cannot be sync-ed from PowerPoint yet.
const SLIDEWIDTH = 960;
const SLIDEHEIGHT = 540;
function defaultCursorConfig() {
return {
kind: "Shape", // enum in "Shape"||"Image"
data: "Rectangle", // GeometricShape type or base64 image string
by: "Section", // enum in "Section"||"Slide"j
width: 10,
height: 10,
topOffset: 30,
leftOffset: 5,
alignment: "Start", // enum in "Start"||"Middle"||"End"
format: defaultActiveFormat(),
pastFormat: defaultInactiveFormat(),
upcomingFormat: defaultInactiveFormat(),
fitImage: false
};
}
async function computeCursorPropBySection(cursorConfig, sectionBarConfig) {
const { direction, slideSectionIds, sectionTitles } = sectionBarConfig;
const barProp = await computeSectionBarProp(sectionBarConfig);
const { alignment: align, format, leftOffset, topOffset, width, height, offsetPerp } = cursorConfig;
let secProp;
if (direction === "Vertical") {
secProp = barProp.map((p) => {
const realHeight = height <= 0 ? p.height + height : height;
const alignOffset =
align === "Center" ? (p.height - realHeight) / 2 : align === "End" ? p.height - realHeight : 0;
return {
width: width,
height: realHeight,
top: p.top + topOffset + alignOffset,
left: p.left + leftOffset
};
});
} else if (direction === "Horizontal") {
secProp = barProp.map((p) => {
const realWidth = width <= 0 ? p.width + width : width;
const alignOffset = align === "Center" ? (p.width - realWidth) / 2 : align === "End" ? p.width - realWidth : 0;
return {
width: realWidth,
height: height,
top: p.top + topOffset,
left: p.left + leftOffset + alignOffset
};
});
} else {
throw new Error(`Invalid direction ${direction} in computeCursorPropBySection()`);
}
const prop = slideSectionIds.map((i, n) => secProp[i]);
if (cursorConfig.showInactive) {
const dis = cursorConfig.distanceWithinSection;
let currentSectionSlideCount = 0;
let oldId = -1;
const newProp = prop.map((p, i) => {
if (slideSectionIds[i] !== oldId) {
currentSectionSlideCount = 0;
oldId = slideSectionIds[i];
}
currentSectionSlideCount += 1;
if (p === undefined) {
return undefined;
}
const isHor = direction === "Horizontal";
if ((isHor && !offsetPerp) || (!isHor && offsetPerp)) {
return {
...p,
left: p.left + dis * (currentSectionSlideCount - 1)
};
} else {
return {
...p,
top: p.top + dis * (currentSectionSlideCount - 1)
};
}
});
return newProp;
} else {
return prop;
}
}
function computeCursorPropBySlide(cursorConfig, sectionBarConfig) {
const { slideSectionIds, direction, rangeWidth, rangeHeight, rangeLeft, rangeTop } = sectionBarConfig;
const { alignment: align, width, height, leftOffset, topOffset } = cursorConfig;
const slideCount = slideSectionIds.length;
if (direction === "Vertical") {
const span = rangeHeight / slideCount;
return Array(slideCount)
.fill(0)
.map((_, i) => {
const realHeight = height <= 0 ? span + height : height;
const alignOffset = align === "Center" ? (span - realHeight) / 2 : align === "End" ? span - realHeight : 0;
return {
width: width,
height: realHeight,
left: rangeLeft + leftOffset,
top: rangeTop + topOffset + span * i + alignOffset
};
});
} else if (direction === "Horizontal") {
const span = rangeWidth / slideCount;
return Array(slideCount)
.fill(0)
.map((_, i) => {
const realWidth = width <= 0 ? span + width : width;
const alignOffset = align === "Center" ? (span - realWidth) / 2 : align === "End" ? span - realWidth : 0;
return {
width: realWidth,
height: height,
left: rangeLeft + leftOffset + span * i + alignOffset,
top: rangeTop + topOffset
};
});
} else {
throw new Error(`Invalid direction ${direction} in computeCursorPropBySlide()`);
}
}
async function computeCursorProp(cursorConfig, sectionBarConfig) {
if (cursorConfig.by === "Slide") {
return await computeCursorPropBySlide(cursorConfig, sectionBarConfig);
} else if (cursorConfig.by === "Section") {
return await computeCursorPropBySection(cursorConfig, sectionBarConfig);
} else {
throw new Error(`Invalid cursorConfig.by ${cursorConfig.by} in computeCursorProp()`);
}
}
// async function createCursor(cursorConfig, sectionBarConfig) {
// if (cursorConfig.kind === "Shape") {
// return await createShapeCursor(cursorConfig, sectionBarConfig);
// } else if (cursorConfig.kind === "Image") {
// return await createImageCursor(cursorConfig, sectionBarConfig);
// }
// }
async function createCursor(cursorConfig, sectionBarConfig) {
console.log(cursorConfig);
await PowerPoint.run(async (context) => {
const { slideSectionIds } = sectionBarConfig;
const prop = await computeCursorProp(cursorConfig, sectionBarConfig);
// active cursor
for (let n = 0; n < slideSectionIds.length; n++) {
let p = prop[n];
if (p === undefined) {
return;
}
const shapes = context.presentation.slides.getItemAt(n).shapes;
if (cursorConfig.kind === "Shape") {
const geometricShapeType = getGeomShapeType(cursorConfig.data);
const shape = shapes.addGeometricShape(geometricShapeType, p);
setTextboxFormat(shape, cursorConfig.format);
shape.textFrame.textRange.text = cursorConfig.format.textFrame.textRange.text;
shape.name = `NavbarCursor-${n}`;
} else if (cursorConfig.kind === "Image") {
await PowerPoint.run(async (context) => {
const slide = context.presentation.slides.getItemAt(n).load("id");
await context.sync();
context.presentation.setSelectedSlides([slide.id]);
await context.sync();
await Office.context.document.setSelectedDataAsync(
cursorConfig.data,
cursorConfig.fitImage
? {
coercionType: Office.CoercionType.Image,
imageLeft: p.left,
imageTop: p.top,
imageWidth: p.width,
imageHeight: p.height
}
: {
coercionType: Office.CoercionType.Image,
imageLeft: p.left,
imageTop: p.top
},
async function(result) {
const shapes = context.presentation.slides.getItemAt(n).shapes;
shapes.load("items/name");
await context.sync();
const shape = shapes.items[shapes.items.length - 1];
shape.name = `NavbarCursorImage-${n}`;
await context.sync();
}
);
});
}
}
if (!cursorConfig.showInactive) {
return;
}
// inactive cursor
if (!cursorConfig.replaceInactiveWithImage) {
for (let n = 0; n < slideSectionIds.length; n++) {
const shapes = context.presentation.slides.getItemAt(n).shapes;
for (let i = 0; i < slideSectionIds.length; i++) {
if (n == i) {
continue;
}
const geometricShapeType = getGeomShapeType(cursorConfig.data);
const shape = shapes.addGeometricShape(geometricShapeType, prop[i]);
if (i < n) {
setTextboxFormat(shape, cursorConfig.pastFormat);
} else if (i > n) {
setTextboxFormat(shape, cursorConfig.upcomingFormat);
}
shape.name = `NavbarCursorInactive-${n}-${i}`;
}
}
await context.sync();
} else {
for (let n = 0; n < slideSectionIds.length; n++) {
await PowerPoint.run(async (context) => {
const slide = context.presentation.slides.getItemAt(n).load("id");
await context.sync();
context.presentation.setSelectedSlides([slide.id]);
await context.sync();
for (let i = 0; i < slideSectionIds.length; i++) {
let p = prop[i];
await Office.context.document.setSelectedDataAsync(
cursorConfig.replaceInactiveWithActiveImage
? cursorConfig.data
: i < n
? cursorConfig.pastImage.data
: cursorConfig.upcomingImage.data,
cursorConfig.fitImage
? {
coercionType: Office.CoercionType.Image,
imageLeft: p.left,
imageTop: p.top,
imageWidth: p.width,
imageHeight: p.height
}
: {
coercionType: Office.CoercionType.Image,
imageLeft: p.left,
imageTop: p.top
},
async function(result) {
if (i === slideSectionIds.length - 1) {
const shapes = context.presentation.slides.getItemAt(n).shapes;
shapes.load("items/name");
await context.sync();
shapes.items.slice(-slideSectionIds + 1).map((shape, j) => {
shape.name = `NavbarCursorInactiveImage-${n}-${j}`;
});
await context.sync();
}
}
);
}
});
}
}
});
}
async function createShapeCursor(cursorConfig, sectionBarConfig) {
await PowerPoint.run(async (context) => {
const { slideSectionIds } = sectionBarConfig;
const prop = await computeCursorProp(cursorConfig, sectionBarConfig);
prop.map((p, n) => {
if (p === undefined) {
return;
}
const shapes = context.presentation.slides.getItemAt(n).shapes;
const geometricShapeType = getGeomShapeType(cursorConfig.data);
if (cursorConfig.showInactive) {
prop.map((p, i) => {
const shape = shapes.addGeometricShape(geometricShapeType, p);
if (i === n) {
setTextboxFormat(shape, cursorConfig.format);
} else if (i < n) {
setTextboxFormat(shape, cursorConfig.pastFormat);
} else {
setTextboxFormat(shape, cursorConfig.upcomingFormat);
}
shape.name = `NavbarCursor-${n}-${i}`;
});
} else {
const shape = shapes.addGeometricShape(geometricShapeType, p);
setTextboxFormat(shape, cursorConfig.format);
shape.textFrame.textRange.text = cursorConfig.format.textFrame.textRange.text;
shape.name = `NavbarCursor-${n}`;
}
});
await context.sync();
});
}
async function createImageCursor(cursorConfig, sectionBarConfig) {
const { slideSectionIds } = sectionBarConfig;
const prop = await computeCursorProp(cursorConfig, sectionBarConfig);
const { fitImage } = cursorConfig;
for (let n = 0; n < slideSectionIds.length; n++) {
const p = prop[n];
await PowerPoint.run(async (context) => {
const slide = context.presentation.slides.getItemAt(n).load("id");
await context.sync();
context.presentation.setSelectedSlides([slide.id]);
await context.sync();
await Office.context.document.setSelectedDataAsync(
cursorConfig.data,
fitImage
? {
coercionType: Office.CoercionType.Image,
imageLeft: p.left,
imageTop: p.top,
imageWidth: p.width,
imageHeight: p.height
}
: {
coercionType: Office.CoercionType.Image,
imageLeft: p.left,
imageTop: p.top
},
async function(result) {
await PowerPoint.run(async (context) => {
const shapes = context.presentation.slides.getItemAt(n).shapes;
shapes.load("items/name");
await context.sync();
const shape = shapes.items[shapes.items.length - 1];
shape.name = `NavbarCursorImage-${n}`;
await context.sync();
});
}
);
});
}
}
function getGeomShapeType(name) {
const mapper = {
Rectangle: "Rectangle"
};
const key = name.replace(/\d+$/g, "").replace(" ", "");
if (key in mapper) {
return mapper[key];
} else {
return "Rectangle";
}
}
// example of format data interface
function defaultActiveFormat() {
return {
name: "",
id: "",
width: 0,
height: 0,
left: 0,
top: 0,
fill: {
foregroundColor: "#3d3d3e",
transparency: 0
},
lineFormat: {
color: "#3d3d3e",
transparency: 0,
style: "Single", // "Single" | "ThinThin" | "ThinThick" etc.
dashStyle: "Solid", // "Dash" | "Solid" | "DashDot" etc.
weight: 0.75
},
textFrame: {
verticalAlignment: "Top", // "Top" | "Middle" | "Bottom" | "TopCentered" | "MiddleCentered" | "BottomCentered"
textRange: {
paragraphFormat: {
horizontalAlignment: "Left", //"Left" | "Center" | "Right" | "Justify" | "Distributed" etc
text: ""
},
font: {
size: 18,
name: "Calibri",
color: "#ffffff",
bold: true,
italic: false,
underline: "None" // "None" | "Single" | "Double" etc.
}
}
}
};
}
function defaultInactiveFormat() {
return {
name: "",
id: "",
width: 0,
height: 0,
left: 0,
top: 0,
fill: {
foregroundColor: "#3d3d3e",
transparency: 0
},
lineFormat: {
color: "#3d3d3e",
transparency: 0,
style: "Single",
dashStyle: "Solid",
weight: 0.75
},
textFrame: {
verticalAlignment: "Top",
textRange: {
paragraphFormat: {
horizontalAlignment: "Left",
text: ""
},
font: {
size: 18,
name: "Calibri",
color: "#afafaf",
bold: true,
italic: false,
underline: "None"
}
}
}
};
}
function defaultCursorFormat() {
return {
name: "Rectangle 1",
id: "",
width: 10,
height: 10,
left: 0,
top: 0,
fill: {
foregroundColor: "#4472c4",
transparency: 0
},
lineFormat: {
color: "",
transparency: 0,
style: "Single",
dashStyle: "Solid",
weight: 0.75
},
textFrame: {
text: "",
verticalAlignment: "Top",
textRange: {
paragraphFormat: {
horizontalAlignment: "Left",
text: ""
},
font: {
size: 18,
name: "Calibri",
color: "#000000",
bold: true,
italic: false,
underline: "None"
}
}
}
};
}
// example of Navbar sections data interface.
function defaultSectionBarConfig() {
return {
docking: "Top", // enum from 'Left', 'Right', 'Top', 'Bottom'
distribution: "EqualSpacing", // enum from 'Uniform', 'EqualSpacing', 'SlideNumer'
rangeWidth: SLIDEWIDTH,
rangeHeight: 30,
rangeLeft: 0,
rangeTop: 0,
sectionTitles: [],
slideSectionIds: [],
activeFormat: defaultActiveFormat(),
inactiveFormat: defaultInactiveFormat(),
showActiveOnly: false
};
}
async function readSelectedShapeFormat() {
return await PowerPoint.run(async (context) => {
const shapes = context.presentation.getSelectedShapes();
shapes.load("items");
await context.sync();
if (shapes.items.length === 0) {
throw new Error("No shape is selected.");
}
const shape = shapes.items[0];
shape.load("name, id, width, height, left, top, fill, lineFormat, textFrame");
await context.sync();
const fill = shape.fill.load("$all");
const lineFormat = shape.lineFormat.load("$all");
const textFrame = shape.textFrame.load("$all, textRange/text, textRange/paragraphFormat, textRange/font");
await context.sync();
const paragraphFormat = textFrame.textRange.paragraphFormat;
const font = textFrame.textRange.font;
paragraphFormat.load("$all");
font.load("$all");
await context.sync();
return {
name: shape.name,
id: shape.id,
width: shape.width,
height: shape.height,
left: shape.left,
top: shape.top,
fill: {
foregroundColor: fill.foregroundColor,
transparency: fill.transparency
},
lineFormat: {
color: lineFormat.color,
transparency: lineFormat.transparency,
style: lineFormat.style,
dashStyle: lineFormat.dashStyle,
weight: lineFormat.weight
},
textFrame: {
verticalAlignment: textFrame.verticalAlignment,
textRange: {
text: textFrame.textRange.text,
paragraphFormat: {
horizontalAlignment: paragraphFormat.horizontalAlignment
},
font: {
size: font.size,
name: font.name,
color: font.color,
bold: font.bold,
italic: font.italic,
underline: font.underline
}
}
}
};
});
}
function setTextboxFormat(shape, format) {
if (format.fill.foregroundColor === "") {
shape.fill.clear();
} else {
shape.fill.setSolidColor(format.fill.foregroundColor);
}
shape.fill.transparency = format.fill.transparency;
if (format.lineFormat.color === "") {
if (format.fill.foregroundColor === "") {
shape.lineFormat.visible = false;
} else {
shape.lineFormat.color = format.fill.foregroundColor;
}
} else {
shape.lineFormat.color = format.lineFormat.color;
shape.lineFormat.transparency = format.lineFormat.transparency;
shape.lineFormat.style = format.lineFormat.style;
shape.lineFormat.dashStyle = format.lineFormat.dashStyle;
shape.lineFormat.weight = format.lineFormat.weight === -1 ? 0.75 : format.lineFormat.weight;
}
shape.textFrame.verticalAlignment = format.textFrame.verticalAlignment;
shape.textFrame.textRange.paragraphFormat.horizontalAlignment =
format.textFrame.textRange.paragraphFormat.horizontalAlignment;
Object.assign(shape.textFrame.textRange.font, format.textFrame.textRange.font);
}
async function createSectionBar(sectionBarConfig) {
const { sectionTitles, slideSectionIds, activeFormat, inactiveFormat } = sectionBarConfig;
if (sectionBarConfig.showActiveOnly) {
createSectionBarActiveOnly(sectionBarConfig);
return;
}
await PowerPoint.run(async (context) => {
const shapeProps = await computeSectionBarProp(sectionBarConfig);
if (slideSectionIds.length === 0 || sectionTitles.length === 0) {
throw "Section is not defined.";
}
slideSectionIds.map((id, n) => {
const shapes = context.presentation.slides.getItemAt(n).shapes;
sectionTitles.map((name, i) => {
const shape = shapes.addTextBox(name, shapeProps[i]);
shape.textFrame.autoSizeSetting = PowerPoint.ShapeAutoSize.autoSizeNone;
shape.name = `NavbarSectionTextbox-${n}-${i}`;
if (id === i) {
setTextboxFormat(shape, activeFormat);
} else {
setTextboxFormat(shape, inactiveFormat);
}
});
});
await context.sync();
});
}
async function createSectionBarActiveOnly(sectionBarConfig) {
const {
sectionTitles,
slideSectionIds,
edgeOffset,
startOffset,
endOffset,
width,
docking,
activeFormat
} = sectionBarConfig;
let prop;
if (docking === "Left" || docking === "Right") {
prop = {
width: width,
height: SLIDEHEIGHT - startOffset - endOffset,
left: docking === "Left" ? edgeOffset : SLIDEWIDTH - edgeOffset - width,
top: startOffset
};
} else if (docking === "Top" || docking === "Bottom") {
prop = {
width: SLIDEWIDTH - startOffset - endOffset,
height: width,
left: startOffset,
top: docking === "Top" ? edgeOffset : SLIDEHEIGHT - width - edgeOffset
};
} else {
throw new Error(`Invalid docking ${docking}`);
}
await PowerPoint.run(async (context) => {
slideSectionIds.map((id, n) => {
const shapes = context.presentation.slides.getItemAt(n).shapes;
const shape = shapes.addTextBox(sectionTitles[id], prop);
setTextboxFormat(shape, activeFormat);
shape.name = `NavbarSectionTextbox-${n}`;
});
await context.sync();
});
}
async function computeSectionBarProp(sectionBarConfig) {
const { direction, distribution, sectionTitles } = sectionBarConfig;
let funcMapper = {
VerticalUniform: computePropVerUniform,
VerticalSlideNumber: computePropVerSlideNumber,
VerticalEqualSpacing: computePropVertEqualSpacing,
HorizontalUniform: computePropHorUniform,
HorizontalSlideNumber: computePropHorSlideNumber,
HorizontalEqualSpacing: computePropHorEqualSpacing
};
if (!(direction + distribution in funcMapper)) {
throw new Error(`Wrong key: ${direction}${distribution} in computeSectionBarProp()`);
}
return await funcMapper[direction + distribution](sectionBarConfig);
}
function computePropVerUniform(sectionBarConfig) {
const { sectionTitles, slideSectionIds, rangeWidth, rangeHeight, rangeLeft, rangeTop } = sectionBarConfig;
const length = rangeHeight;
const height = length / sectionTitles.length;
const tops = Array(sectionTitles.length)
.fill(0)
.map((_, i) => rangeTop + i * height);
return tops.map((t) => {
return { width: rangeWidth, height: height, top: t, left: rangeLeft };
});
}
function computePropHorUniform(sectionBarConfig) {
const { sectionTitles, slideSectionIds, rangeWidth, rangeHeight, rangeLeft, rangeTop } = sectionBarConfig;
const length = rangeWidth;
const width = length / sectionTitles.length;
const lefts = Array(sectionTitles.length)
.fill(0)
.map((_, i) => rangeLeft + i * width);
return lefts.map((left) => {
return { width: width, height: rangeHeight, top: rangeTop, left: left };
});
}
function computePropVerSlideNumber(sectionBarConfig) {
const { sectionTitles, slideSectionIds, rangeWidth, rangeHeight, rangeLeft, rangeTop } = sectionBarConfig;
const length = rangeHeight;
const singleHeight = length / slideSectionIds.length;
const heights = sectionTitles.map((name, i) => {
return slideSectionIds.map((id) => (id == i ? singleHeight : 0)).reduce((sum, item) => sum + item);
});
let cum = 0;
const tops = heights.map((_, i) => {
return (cum += i == 0 ? rangeTop : heights[i - 1]);
});
return heights.map((h, i) => {
return { width: rangeWidth, height: h, top: tops[i], left: rangeLeft };
});
}
function computePropHorSlideNumber(sectionBarConfig) {
const { sectionTitles, slideSectionIds, rangeWidth, rangeHeight, rangeLeft, rangeTop } = sectionBarConfig;
const length = rangeWidth;
const singleWidth = length / slideSectionIds.length;
const widths = sectionTitles.map((name, i) => {
return slideSectionIds.map((id) => (id == i ? singleWidth : 0)).reduce((sum, item) => sum + item);
});
let cum = 0;
const lefts = widths.map((_, i) => {
return (cum += i == 0 ? rangeLeft : widths[i - 1]);
});
return widths.map((w, i) => {
return { width: w, height: rangeHeight, top: rangeTop, left: lefts[i] };
});
}
async function computePropVertEqualSpacing(sectionBarConfig) {
const { sectionTitles, slideSectionIds, rangeWidth, rangeHeight, rangeLeft, rangeTop } = sectionBarConfig;
const length = rangeHeight;
const sectionCount = sectionTitles.length;
return await PowerPoint.run(async (context) => {
const allShapes = context.presentation.slides.getItemAt(0).shapes;
const shapes = sectionTitles.map((name) => {
const shape = allShapes.addTextBox(name);
shape.width = rangeWidth;
shape.textFrame.autoSizeSetting = PowerPoint.ShapeAutoSize.autoSizeShapeToFitText;
shape.textFrame.textRange.text = name;
shape.load("height");
return shape;
});
await context.sync();
const textHeights = shapes.map((shape) => shape.height);
shapes.map((shape) => shape.delete());
const totalHeight = textHeights.reduce((sum, item) => sum + item);
const spacing = (length - totalHeight) / sectionTitles.length;
const heights = textHeights.map((h) => h + spacing);
let cum = 0;
const tops = heights.map((_, i) => {
return (cum += i == 0 ? rangeTop : heights[i - 1]);
});
return Array(sectionCount)
.fill(0)
.map((_, i) => {
return { height: heights[i], width: rangeWidth, left: rangeLeft, top: tops[i] };
});
});
}
async function computePropHorEqualSpacing(sectionBarConfig) {
const { sectionTitles, slideSectionIds, rangeLeft, rangeTop, rangeWidth, rangeHeight } = sectionBarConfig;
const initialWidth = rangeWidth / 2;
const trialStep = 10;
const topOffset = rangeTop;
const length = rangeWidth;
return await PowerPoint.run(async (context) => {
const shapesAll = context.presentation.slides.getItemAt(0).shapes;
const benchmark = shapesAll.addTextBox("Benchmark");
benchmark.width = 200;
benchmark.textFrame.autoSizeSetting = PowerPoint.ShapeAutoSize.autoSizeShapeToFitText;
benchmark.textFrame.textRange.text = "Benchmark";
benchmark.load("height");
await context.sync();
const benchmarkHeight = benchmark.height;
benchmark.delete();
await context.sync();
const shapes = sectionTitles.map((name, i) => {
const shape = shapesAll.addTextBox(name);
shape.width = initialWidth;
shape.textFrame.autoSizeSetting = PowerPoint.ShapeAutoSize.autoSizeShapeToFitText;
shape.textFrame.textRange.text = name;
shape.load("height, width");
return shape;
});
await context.sync();
let textWidths = Array(sectionTitles.length).fill(0);
for (let i = 0; i < sectionTitles.length; i++) {
let shapeWidth = initialWidth;
let shapeHeight = benchmarkHeight;
let shape = shapes[i];
while (shapeHeight === benchmarkHeight && shapeWidth >= trialStep) {
shapeWidth -= trialStep;
shape.width = shapeWidth;
shape.textFrame.autoSizeSetting = PowerPoint.ShapeAutoSize.autoSizeShapeToFitText;
shape.textFrame.textRange.text = shape.textFrame.textRange.text;
shape.load("height");
await context.sync();
shapeHeight = shape.height;
}
textWidths[i] = shapeWidth + trialStep;
}
// trick: if total width is too large, allow up to two lines.
let totalWidth = textWidths.reduce((sum, item) => sum + item);
if (totalWidth > length) {
totalWidth /= 2;
textWidths = textWidths.map((w) => w / 2);
}
const spacing = (length - totalWidth) / sectionTitles.length;
if (spacing < 0) {
throw new Error("Section name is too long.");
}
const widths = textWidths.map((w) => w + spacing);
let cum = 0;
const lefts = widths.map((_, i) => {
return (cum += i == 0 ? rangeLeft : widths[i - 1]);
});
shapes.map((shape) => {
shape.delete();
});
await context.sync();
return widths.map((width, i) => {
return { top: topOffset, left: lefts[i], width: width, height: rangeHeight };
});
});
}
// final version!!
async function saveToTag(data) {
await PowerPoint.run(async (context) => {
context.presentation.tags.add("NAVBAR", JSON.stringify(data));
await context.sync();
});
}
// final version!!
async function readPresentationProp() {
return await PowerPoint.run(async (context) => {
const slideCounter = context.presentation.slides.getCount();
const slides = context.presentation.slides.load("items/id");
await context.sync();
const slideCount = slideCounter.value;
const slideIds = slides.items.map((slide) => slide.id);
const slideTitles = Array(slideCount).fill("");
for (let n = 0; n < slideCount; n++) {
const shapes = context.presentation.slides.getItemAt(n).shapes;
shapes.load("items/name, items/type");
await context.sync();
for (let shape of shapes.items) {
if (shape.type === "GeometricShape" && shape.name.startsWith("Title")) {
shape.load("textFrame/textRange/text");
await context.sync();
slideTitles[n] = shape.textFrame.textRange.text;
break;
}
}
}
const tag = context.presentation.tags.getItemOrNullObject("NAVBAR");
tag.load("value");
await context.sync();
const savedData = tag.isNullObject ? {} : JSON.parse(tag.value);
return { slideCount, slideIds, slideTitles, savedData };
});
}
// final version!
async function deleteAllNavbarShapes(slideCount) {
await PowerPoint.run(async function(context) {
const slideCounter = context.presentation.slides.getCount();
await context.sync();
const shapesList = Array(slideCount)
.fill(0)
.map((_, n) => {
return context.presentation.slides.getItemAt(n).shapes.load("items/name");
});
await context.sync();
shapesList.map((shapes) => {
shapes.items.map((shape) => {
if (shape.name.startsWith("Navbar")) {
shape.delete();
}
});
});
await context.sync();
});
}
/** Default helper for invoking an action and handling errors. */
async function tryCatch(callback) {
try {
await callback();
} catch (error) {
// Note: In a production add-in, you'd want to notify the user through your add-in's UI.
console.error(error);
}
}
async function logShapeName() {
await PowerPoint.run(async (context) => {
const shapes = context.presentation.getSelectedShapes();
shapes.load("items/name");
await context.sync();
console.log(shapes.items.map((shape) => shape.name));
});
}
language: typescript
template:
content: "<div id=\"navbar-app\" class=\"\">\n\t<div class=\"navbar-fixed\">\n\t\t<nav class=\"nav-extended\">\n\t\t\t<div class=\"nav-wrapper\">\n\t\t\t\t<ul id=\"nav-mobile\" class=\"left\">\n\t\t\t\t\t<li><a href=\"#\" @click=\"deleteAll\">Delete</a></li>\n\t\t\t\t\t<li><a href=\"#\" @click=\"refresh\">Refresh</a></li>\n\t\t\t\t\t<li><a href=\"#\" @click=\"log\">Log</a></li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</nav>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s12\">\n\t\t\t<h5>Define Sections</h5>\n\t\t\t<table>\n\t\t\t\t<thead>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th>Section title</th>\n\t\t\t\t\t\t<th>No.</th>\n\t\t\t\t\t\t<th>Slide title</th>\n\t\t\t\t\t\t<th>Skip</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr v-for=\"n in slideCount\">\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<div contenteditable v-html=\"slideSectionTitles[n-1]\"\n\t\t\t\t\t\t\t\t@blur=\"slideSectionTitles[n-1]=$event.target.innerHTML;\"></div>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td>{{ n }}</td>\n\t\t\t\t\t\t<td>{{ slideTitles[n-1] }}</td>\n\t\t\t\t\t\t<td><label><input :id=\"'skip-' + n\" type=\"checkbox\" v-model=\"slideSkipped[n-1]\"/><span></span></label>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s12\">\n\t\t\t<h5>Define Navbar Style</h5>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Direction: </p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p><label>\n\t\t\t\t<input name=\"navbar-dir\" type=\"radio\" v-model=\"sectionBar.direction\" value=\"Horizontal\"/>\n\t\t\t\t<span>Horizontal</span>\n\t\t\t</label>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t<input name=\"navbar-dir\" type=\"radio\" v-model=\"sectionBar.direction\" value=\"Vertical\"/>\n\t\t\t\t<span>Vertical</span>\n\t\t\t</label></p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Distribution:</p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"navbar-distribution\" type=\"radio\" v-model=\"sectionBar.distribution\" value=\"EqualSpacing\"/>\n\t\t\t\t\t<span>Equal spacing</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"navbar-distribution\" type=\"radio\" v-model=\"sectionBar.distribution\" value=\"Uniform\"/>\n\t\t\t\t\t<span>Uniform</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"navbar-distribution\" type=\"radio\" v-model=\"sectionBar.distribution\" value=\"SlideNumber\"/>\n\t\t\t\t\t<span>By slide number</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t</div>\n\t\t<div class=\"col s12\">\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input type=\"checkbox\" v-model=\"sectionBar.showActiveOnly\"/>\n\t\t\t\t\t<span>Show active section only </span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\n\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Navbar Range:</p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p>\n\t\t\t\t<a class=\"teal lighten-3 waves-effect btn-flat tooltipped\" data-position=\"top\"\n\t\t\t\t\tdata-tooltip=\"Select a rectangle that defines the range first\" @click=\"readSectionBarRange\">Read</a>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Match format:</p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p>\n\t\t\t\t<a class=\"waves-effect btn-flat tooltipped\" data-position=\"top\" data-tooltip=\"Select a textbox first\"\n\t\t\t\t\t@click=\"matchShapeFormat('Active')\">Active</a>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<a class=\"waves-effect btn-flat tooltipped\" data-position=\"top\" data-tooltip=\"Select a textbox first\"\n\t\t\t\t\t@click=\"matchShapeFormat('Inactive')\">Inactive</a>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s12\">\n\t\t\t<a class=\"waves-effect waves-light btn\" @click=\"createNavbar\">Create Navbar</a>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s12\">\n\t\t\t<h5>Define cursor</h5>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Arrange cursor</p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"cursorArrange\" type=\"radio\" v-model=\"cursor.by\" value=\"Section\"/>\n\t\t\t\t\t<span>By section</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"cursorArrange\" type=\"radio\" v-model=\"cursor.by\" value=\"Slide\"/>\n\t\t\t\t\t<span>By slide</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Align cursor at</p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"cursorAlign\" type=\"radio\" v-model=\"cursor.alignment\" value=\"Start\"/>\n\t\t\t\t\t<span>Start</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"cursorAlign\" type=\"radio\" v-model=\"cursor.alignment\" value=\"Center\"/>\n\t\t\t\t\t<span>Center</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input name=\"cursorAlign\" type=\"radio\" v-model=\"cursor.alignment\" value=\"End\"/>\n\t\t\t\t\t<span>End</span>\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\n\n\t<div class=\"row\">\n\t\t<div class=\"col s4\">\n\t\t\t<p>Size & position:</p>\n\t\t</div>\n\t\t<div class=\"col s8\">\n\t\t\t<p>\n\t\t\t\t<a class=\"waves-effect btn-flat tooltipped\" data-position=\"top\"\n\t\t\t\t\tdata-tooltip=\"BEFORE CLICK: select a shape that defines cursor shape and offset from the left top corner of Navbar\"\n\t\t\t\t\t@click=\"readCursorProp\">Read</a>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\n\n\t<div class=\"row\">\n\t\t<div class=\"col s12\">\n\t\t\t<label>\n\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.fullfill\" />\n\t\t\t\t<span>Strech to fullfill Navbar</span>\n\t\t\t</label>\n\t\t</div>\n\t\t<div class=\"input-field col s12\" v-if=\"cursor.fullfill\">\n\t\t\t<label for=\"fullfill-gap\">Gap size (No gap by default)</label>\n\t\t\t<input id=\"fullfill-gap\" type=\"text\" v-model.number=\"cursor.fullfillGap\" />\n\t\t</div>\n\t\t</div>\n\n\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col s12\">\n\t\t\t\t<label>\n\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.replaceWithImage\" />\n\t\t\t\t<span>Replace cursor with Image</span>\n\t\t\t</label>\n\t\t\t</div>\n\t\t\t<div class=\"file-field input-field col s12\" v-if=\"cursor.replaceWithImage\">\n\t\t\t\t<div class=\"btn btn-flat\">\n\t\t\t\t\t<span>Select image</span>\n\t\t\t\t\t<input type=\"file\" ref=\"fileImageCursor\" @change=\"loadImageCursor()\">\n\t\t\t\t</div>\n\t\t\t\t\t<div class=\"file-path-wrapper\">\n\t\t\t\t\t\t<input class=\"file-path\" type=\"text\">\n\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-if=\"!cursor.replaceWithImage\">\n\t\t\t\t\t\t<div class=\"col s4\">\n\t\t\t\t\t\t\t<p>Match format:</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"col s8\">\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t<a class=\"waves-effect btn-flat tooltipped\" data-position=\"top\"\n\t\t\t\t\t\t\t\t\tdata-tooltip=\"BEFORE CLICK: select a shape with target format\"\n\t\t\t\t\t\t\t\t\t@click=\"matchShapeFormat('Current')\">Cursor</a>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col s12\" v-if=\"cursor.replaceWithImage\">\n\t\t\t\t\t\t<label>\n\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.fitImage\" />\n\t\t\t\t<span>Fit image to previously read size</span>\n\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\n\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col s12\">\n\t\t\t\t\t\t<label>\n\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.showInactive\" />\n\t\t\t\t<span>Include inactive cursors</span>\n\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"input-field col s12\" v-if=\"cursor.showInactive\">\n\t\t\t\t\t\t<label for=\"distance-within-section\">Offset dist. within section (20 by default)</label>\n\t\t\t\t\t\t<input id=\"distance-within-section\" type=\"text\" v-model.number=\"cursor.distanceWithinSection\" />\n\t\t</div>\n\t\t\t\t\t\t<div class=\"col s12\" v-if=\"cursor.showInactive\">\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.offsetPerp\" />\n\t\t\t\t\t\t<span>Offset perpendicular to direction</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"col s12\" v-if=\"cursor.showInactive\">\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t<label>\n\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.replaceInactiveWithImage\" />\n\t\t\t\t<span>Replace inactive cursors with image</span>\n\t\t\t</label>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"row\" v-if=\"cursor.replaceInactiveWithImage\">\n\t\t\t\t\t\t<div class=\"col s12\">\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t<input type=\"checkbox\" v-model=\"cursor.replaceInactiveWithActiveImage\" />\n\t\t\t\t\t\t\t<span>Use active image</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"cursor.replaceInactiveWithImage && !cursor.replaceInactiveWithActiveImage\">\n\t\t\t\t\t\t\t<div class=\"file-field input-field col s12\">\n\t\t\t\t\t\t\t\t<div class=\"btn btn-flat\">\n\t\t\t\t\t\t\t\t\t<span>Image for past</span>\n\t\t\t\t\t\t\t\t\t<input type=\"file\" ref=\"fileImagePast\" @change=\"loadImagePast\">\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"file-path-wrapper\">\n\t\t\t\t\t\t\t\t\t\t<input class=\"file-path\" type=\"text\">\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"file-field input-field col s12\" v-if=\"cursor.replaceWithImage\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn btn-flat\">\n\t\t\t\t\t\t\t\t\t\t\t<span>Image for upcoming</span>\n\t\t\t\t\t\t\t\t\t\t\t<input type=\"file\" ref=\"fileImageUpcoming\" @change=\"loadImageUpcoming\">\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"file-path-wrapper\">\n\t\t\t\t\t\t\t\t\t\t\t\t<input class=\"file-path\" type=\"text\">\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<div class=\"row\" v-if=\"cursor.showInactive && !cursor.replaceInactiveWithImage\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"col s4\">\n\t\t\t\t\t\t\t\t\t\t\t<p>Match inactive shape cursor format</p>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div class=\"col s8\">\n\t\t\t\t\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"waves-effect btn-flat tooltipped\" data-position=\"top\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-tooltip=\"BEFORE CLICK: select a shape with target format\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t@click=\"matchShapeFormat('Past')\">Past</a>\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"waves-effect btn-flat tooltipped\" data-position=\"top\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-tooltip=\"BEFORE CLICK: select a shape with target format\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t@click=\"matchShapeFormat('Upcoming')\">Upcoming</a>\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"col s12\">\n\t\t\t\t\t\t\t\t\t\t\t<a class=\"waves-effect waves-light btn\" @click=\"createCursor\">Create\n\t\t\t\t\t\t\t\t\t\t\t\tcursor</a>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\n\n\t\t\t\t\t\t\t\t\t<div class=\"preloader-mask\" v-if=\"isSyncing\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"preloader-wrapper big active\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"spinner-layer spinner-blue-only\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"circle-clipper left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"circle\"></div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"gap-patch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"circle\"></div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"circle-clipper right\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"circle\"></div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<footer class=\"page-footer\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"container\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"col l6 s12\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<h5 class=\"white-text\">Navbar v1.0.0</h5>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p class=\"grey-text text-lighten-4\">Created with love by Hanlin Dong\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p class=\"grey-text text-lighten-4\">Contact: self@hanlindong.com\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div class=\"footer-copyright\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"container\">\n\t\t\t\t\t\t\t\t\t\t\t\t© 2022 Copyright\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</footer>\n\t\t\t\t\t\t\t\t</div>"
language: html
style:
content: "section.samples {\n margin-top: 20px;\n}\n\nsection.samples .ms-Button, section.setup .ms-Button {\n display: block;\n margin-bottom: 5px;\n margin-left: 20px;\n min-width: 80px;\n}\n\n.ms-ListItem-primaryText {\n font-size: 16px;\n}\n\n.ms-MessageBar {\n position: absolute;\n top: 5px;\n left: 5px;\n}\n\n.preloader-mask {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: dimgrey;\n opacity: 0.5;\n text-align: center;\n}\n\n.preloader-mask .preloader-wrapper {\n position: absolute;\n top: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n margin: auto;\n}"
language: css
libraries: >
https://appsforoffice.microsoft.com/lib/1/hosted/office.js
@types/office-js
office-ui-fabric-js@1.4.0/dist/js/fabric.min.js
core-js@2.4.1/client/core.min.js
@types/core-js
jquery@3.1.1
@types/jquery@3.3.1
https://unpkg.com/vue@3/dist/vue.global.js
https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css
https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment