Instantly share code, notes, and snippets.
Created
September 7, 2023 16:20
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save Hanlin-Dong/636da9de2b6de3aaa9ee8cea5fbb4e9d to your computer and use it in GitHub Desktop.
Create navigation bar and progress bars in PowerPoint!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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