Following are some of the use cases of our JS API that we have collected over the years. All of them are accompanied by a codepen, so you can easily see what the code does and even play around with it and apply it to your specific needs.
You can also check out all of our codepensYou might want to display the Printess editor embedded in a page that offers some of your own inputs, and depending on their value, switch, which template is loaded.
This is achievable by initialising the editor and later triggering the loadTemplateAndFormFields() function when a relevant value changes.
let printessEditor = null;
async function loadTemplate(templateName) {
// disabling the template switch so users don't click while we are loading
document.querySelectorAll("button").forEach((btn) => {
btn.disabled = true;
});
if(!printessEditor) {
//Editor has not been initialized yet
printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
printessEditor = await printessLoader.load({
token: "[your-shop-token]",
templateName: templateName,
templateVersion: "draft",
container: document.querySelector(".editor-wrapper"),
basketId: "Some-Unique-Basket-Or-Session-Id",
addToBasketCallback: (token, thumbnailUrl) => prompt("Savetoken:",token)
});
} else {
//Editor has been initialized before
//Second parameter are the merge templates and third parameter are the form fields
await printessEditor.api.loadTemplateAndFormFields(templateName, [], [], null);
}
// users are allowed to click again
document.querySelectorAll("button").forEach((btn) => {
btn.disabled = false;
});
}
loadTemplateAndFormFields<div class="button-list">
<button data-template-name="assign-image-to-named-frame">Doc 1</button>
<button data-template-name="Various Frames">Doc 2</button>
<button data-template-name="Baby Photo Book">Doc 3</button>
</div>
<div class="editor-wrapper"></div>
<script>
let printessEditor = null;
async function loadTemplate(templateName) {
document.querySelectorAll("button").forEach((btn) => {
btn.disabled = true;
});
if (!printessEditor) {
//Editor has not been initialized yet
printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
printessEditor = await printessLoader.load({
token: "[your-shop-token]",
templateName: templateName,
templateVersion: "draft",
container: document.querySelector(".editor-wrapper"),
basketId: "Some-Unique-Basket-Or-Session-Id",
addToBasketCallback: (token, thumbnailUrl) => prompt("Savetoken:", token)
});
} else {
//Editor has been initialized before
//Second parameter are the merge templates and third parameter are the form fields
await printessEditor.api.loadTemplateAndFormFields(templateName, [], [], null);
}
// users are allowed to click again
document.querySelectorAll("button").forEach((btn) => {
btn.disabled = false;
});
}
document.querySelectorAll("button").forEach((btn) => {
btn.addEventListener("click", function () {
loadTemplate(btn.getAttribute("data-template-name")).then(() => {
});
}, false);
});
</script>
loadTemplateAndFormFieldsYou can jump to a document and spread in your template after a user action by using selectDocumentAndSpread(docIdOrName: string, spreadIndex: number, part?: "entire" | "left-page" | "right-page"): Promise<void>.
The first parameter accepts either the document ID or the document name.
Note that spreadIndex in this context does not reference the unique ID of the spread, but instead its position in the spreads array of the document.
The data needed for this method can be found by using getAllDocsAndSpreads(applyLockCoverInside?: boolean): iExternalDocAndSpreadInfo[], which will return an array containing all documents in your template.
Each document contains spreads, an array of iExternalSpreadInfo.
const selectDocumentByTitle = async () => {
const title = "YOUR_DOCUMENT_TITLE";
const allDocsAndSpreads = await printessApi.api.getAllDocsAndSpreads();
if(allDocsAndSpreads) {
const titledDoc = allDocsAndSpreads.find(doc => doc.docTitle === title);
if(titledDoc) {
await printessApi.api.selectDocumentAndSpread(titledDoc.docId, 0);
}
}
}
getAllDocsAndSpreads, selectDocumentAndSpread<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
// example of jumping to the first spread of a named document
const selectDocumentByTitle = async () => {
const title = "Not_Primary-MergeTarget";
const allDocsAndSpreads = await printessApi.api.getAllDocsAndSpreads();
if (allDocsAndSpreads) {
const titledDoc = allDocsAndSpreads.find(doc => doc.docTitle === title);
if (titledDoc) {
await printessApi.api.selectDocumentAndSpread(titledDoc.docId, 0);
}
}
}
// example of jumping to the last spread of the last document
const selectLastSpread = async () => {
const allDocsAndSpreads = await printessApi.api.getAllDocsAndSpreads();
if (allDocsAndSpreads) {
const lastDoc = allDocsAndSpreads[allDocsAndSpreads.length - 1];
await printessApi.api.selectDocumentAndSpread(lastDoc.docId, lastDoc.spreads.length - 1);
}
}
const printessApi = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Printess - Merging through Attach Parameters",
templateVersion: "published",
// saveTemplateCallback: selectLastSpread,
saveTemplateCallback: selectDocumentByTitle,
});
</script>
getAllDocsAndSpreads, selectDocumentAndSpreadYou can set the value of a form field through the API method setFormFieldValue(fieldName: string, newValue: string).
const changeSize = () => {
if(printess) {
printess.api.setFormFieldValue("DOCUMENT_SIZE", "10x10")
}
}
setFormFieldValue<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const changeSize = () => {
if (printess) {
printess.api.setFormFieldValue("DOCUMENT_SIZE", "10x10")
}
}
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Canvas",
templateVersion: "published",
buttons: [{
label: "Size: 10x10",
location: "button-bar",
clickCallback: changeSize,
icon: "docRef",
color: "secondary",
outline: "outline"
}]
});
</script>
setFormFieldValueDepending on factors you might want to disable some options in a Select Form Field, for example if some materials are temporarily out of stock.
You can use the API function setFormFieldListDisabledStates:
setFormFieldListDisabledStates(
ffName: string,
states: {
disabled: boolean;
value: string;
}[]
): Promise<void>
In this function you can specify the disabled state for each value the form field with the name ffName needs.
<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
translationKey: "en",
templateName: "T-Shirt-SizeExample",
templateVersion: "draft"
})
printess.api.setFormFieldListDisabledStates('Size', [
{ value: "s", disabled: false },
{ value: "m", disabled: false },
{ value: "l", disabled: false },
{ value: "xl", disabled: true },
{ value: "2xl", disabled: true },
{ value: "3xl", disabled: false }
])
</script>
setFormFieldListDisabledStatesThe Printess API gives you a method to check if a user has scrolled all the way to the bottom of your document. This might be helpful if you want to make sure they have seen everything inside the template or if you want to notify them of something they might have overlooked on their way there.
The method is isScrolledToBottom(): boolean, and you could check it periodically like this:
window.setInterval(() => {
console.log("isScrolledToBottom() = " + printess.api.isScrolledToBottom())
}, 700);
isScrolledToBottom<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Baby Photo Book",
templateVersion: "published",
basketId: "Some-Unique-Basket-Or-Session-Id",
addToBasketCallback: (saveToken, thumbnailUrl) => {
prompt("Savetoken:", saveToken)
}
})
window.setInterval(() => {
console.log("isScrolledToBottom() = " + printess.api.isScrolledToBottom())
}, 700);
</script>
isScrolledToBottomIf you are creating a photobook, you might want to change its spine width depending on settings, for example the material of pages or other quality settings.
The API offers setSpineFormular(formular: string): Promise<void>.
The formular can be simply a singular value or a more complex formular, depending on the number of pages for example.
printess.api.setSpineFormular("200px");
// or
printess.api.setSpineFormular("=spine.pages * 0.3mm");
setSpineFormular<div class="spine printess-owned">
<button id="spine1">setSpineFormular("200px")</button>
<button id="spine2">setSpineFormular("10px")</button>
</div>
<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "spine",
templateVersion: "draft",
addToBasketCallback: (token, thumbnailUrl) => {
prompt("Savetoken: ", token);
},
})
document.getElementById("spine1").addEventListener("click", () => {
printess.api.setSpineFormular("200px");
})
document.getElementById("spine2").addEventListener("click", () => {
printess.api.setSpineFormular("10px");
})
</script>
setSpineFormularbody {
background: wheat;
font-family: sans-serif;
}
.spine {
position: absolute;
top: 0px;
left: 50%;
transform: translateX(-50%);
z-index: 99999999;
display: inline-block;
background-color: pink;
padding: 10px;
border: 1px solid black;
}
#printess-editor {
background-color: white;
position: absolute;
left: 30px;
right: 30px;
top: 60px;
height: calc(100% - 90px);
outline: 3px solid red;
overflow: hidden;
}
For a photobook, the spine width usually depends on how many pages the buyer has added, so the fixed formular from the example above is often not enough on its own - you need to react whenever the page count changes and re-apply setSpineFormular().
The easiest way to do this is inside priceChangeCallback: it fires whenever anything price-relevant changes, including the number of pages in a book, and it hands you a priceInfo object that carries the current pageCount.
priceChangeCallback: (priceInfo) => {
if (priceInfo.pageCount !== oldPageCount) {
oldPageCount = priceInfo.pageCount;
printess.api.setSpineFormular("=spine.pages * 0.3mm");
}
}
setSpineFormular<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
let oldPageCount = 0;
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Magazine - PUR - Portrait",
templateVersion: "published",
addToBasketCallback: (token, thumbnailUrl) => {
prompt("Savetoken: ", token);
},
priceChangeCallback: (priceInfo) => {
if (priceInfo.pageCount !== oldPageCount) {
printess.api.setSpineFormular("=spine.pages * 0.3mm");
}
}
})
</script>
setSpineFormularThe published demo linked below never actually re-assigns oldPageCount inside the callback, so the comparison stays true and setSpineFormular() ends up firing on every price change instead of only when the page count changed. It’s harmless here since re-applying the same formular is cheap, but for your own integration make sure to update the comparison variable, as shown in the snippet above the codeblock, so the API isn’t called more often than necessary.
The same pattern works for any bound document, not just Freestyle Photobooks - the demo below applies it to a fixed-page Magazine template instead:
See setSpineFormular used on a Magazine templateIf you need to find the postion of the currently selected frame, for example to display a UI hint to your users, you can find it by calling getSelectionPosition().
This returns an object containing the positional data of the frame, which you can access.
If no frame is selected, it will return null, so make sure to check for that before drilling into the object!
{
anchorX: "left" | "center" | "right";
anchorY: "top" | "bottom" | "middle";
containerPosition: {
height: number;
left: number;
top: number;
width: number;
};
height: number;
left: number;
rotation: number;
rotationPositionX: number;
rotationPositionY: number;
top: number;
width: number;
}
In our example below, we have used the positional data to draw a red border around the frame, highlighting how you can access any border.
<div id="overlay" class="printess-owned"></div>
<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Various Frames",
templateVersion: "draft",
basketId: "Some-Unique-Basket-Or-Session-Id",
addToBasketCallback: (token, thumbnailUrl) => prompt("Savetoken:", token),
})
window.setInterval(() => {
const p = printess.api.getSelectionPosition();
const d = document.getElementById("overlay");
const comp = document.querySelector("printess-component");
const container = comp.shadowRoot.querySelector(".bcui-printess-container");
const cbb = container.getBoundingClientRect();
if (p) {
d.style.left = (cbb.left + p.containerPosition.left) + "px";
d.style.top = (cbb.top + p.containerPosition.top) + "px";
d.style.width = p.containerPosition.width + "px";
d.style.height = p.containerPosition.height + "px";
d.style.display = "block";
} else {
d.style.display = "none";
}
}, 500)
</script>
getSelectionPosition#overlay {
position: absolute;
z-index: 10000000;
border: 2px solid red;
pointer-events: none;
}
If you have price relevant form fields set up in your template and are using the Printess price display, you probably want to update the displayed price whenever a price relevant form field changes.
In order to achieve this, you will need at a callback and an API call.
There are two callbacks available to notofy you, when a price relevant form field has changed, formFieldChangedCallback and priceChangeCallback.
You need to update your price in the priceChangeCallback, otherwise it will not be displayed correctly.
formFieldChangedCallback will trigger, when a price-relevant form field changes and return the form field that has actually changed and the new value.
This callback will not be triggered when a price-relevant Snippet changes!
It’s the easiest way to access the data that has changed without having to traverse an object looking for the new values.
priceChangeCallback will trigger whenever anything price-related changes, which can include Snippets.
It comes with all price relevant settings, no matter what actually changed to trigger it.
Theroretically this is the only callback you need, but if you want to pinpoint the actual change, it can be easier to use formFieldChangedCallback as well!
One or both of these callbacks and the price info from your shop system give you all the information you need in order to recalculate the price.
Once you know the new price, you can call api.ui.refreshPriceDisplay() inside the priceChangeCallback with your data:
const price = 9.99 // Your calculated price as a (decimal) number
api.ui.refreshPriceDisplay({
price: price + "€",
productName: "My product",
legalNotice: "Taxes & shipping included",
infoUrl: ""
});
refreshPriceDisplay accepts price, oldPrice (shown crossed out), legalNotice, productName, infoUrl and optionally priceCategoryLabels to dynamically update the price badge labels.
<script type="module">
//Your available product variants
const productVariants = [
{ productOptions: [{ name: "Material", value: "Acrylic glass" }, { name: "Size", value: "15 cm x 10 cm" }], price: 10.99 },
{ productOptions: [{ name: "Material", value: "Acrylic glass" }, { name: "Size", value: "21 cm x 14 cm" }], price: 11.99 },
{ productOptions: [{ name: "Material", value: "Acrylic glass" }, { name: "Size", value: "30 cm x 20 cm" }], price: 12.99 },
{ productOptions: [{ name: "Material", value: "Acrylic glass" }, { name: "Size", value: "42 cm x 28 cm" }], price: 13.99 },
{ productOptions: [{ name: "Material", value: "Metal sheet" }, { name: "Size", value: "15 cm x 10 cm" }], price: 20.99 },
{ productOptions: [{ name: "Material", value: "Metal sheet" }, { name: "Size", value: "21 cm x 14 cm" }], price: 21.99 },
{ productOptions: [{ name: "Material", value: "Metal sheet" }, { name: "Size", value: "30 cm x 20 cm" }], price: 22.99 },
{ productOptions: [{ name: "Material", value: "Metal sheet" }, { name: "Size", value: "42 cm x 28 cm" }], price: 23.99 }
];
//Method to retrieve one product variant by a given set of settings
function getVariant(productOptions) {
let variants = productVariants;
for (const optionName in productOptions) {
variants = variants.filter((x) => {
return typeof x.productOptions.find((y) => {
return y.name === optionName && y.value === productOptions[optionName];
}) !== "undefined";
});
}
if (variants.length > 0) {
return variants[0];
}
return null;
}
//The current product configuration on your product page
const currentProductConfiguration = {
Material: "Acrylic glass",
Size: "30 cm x 20 cm"
};
const initialFormFieldValues = [];
for (const productOption in currentProductConfiguration) {
initialFormFieldValues.push({
name: productOption,
value: currentProductConfiguration[productOption]
});
}
// On FF change, define the currently selected state accordingly
const onFormFieldChange = (name, value, tag, label, ffLabel) => {
if (typeof currentProductConfiguration[name] !== "undefined") {
currentProductConfiguration[name] = value;
} else if (typeof currentProductConfiguration[ffLabel] !== "undefined") {
currentProductConfiguration[ffLabel] = label;
}
}
// When the price changes, recalculate the price and tell Printess to update its UI
const onPriceChange = (priceInfo) => {
const variant = getVariant(currentProductConfiguration);
apiContainer.ui.refreshPriceDisplay({
snippetPrices: [],
priceCategories: {},
price: variant ? variant.price + "€" : 0.00,
productName: "My product",
legalNotice: "Taxes & shipping included",
infoUrl: ""
});
}
let apiContainer = null;
async function init() {
const printessLoader = await import('https://editor.printess.com/printess-editor/loader.js');
const params = {
token: '[your-shop-token]',
translationKey: 'de-DE',
templateName: 'FormFieldsAndPricingExample',
templateVersion: 'draft', // remove in production! => published
formFields: initialFormFieldValues,
priceChangeCallback: (priceInfo) => onPriceChange(priceInfo),
formFieldChangedCallback: (name, value, tag, label, ffLabel) => onFormFieldChange(name, value, tag, label, ffLabel)
};
apiContainer = await printessLoader.load(params);
}
init();
</script>
You can detect an image upload by the user by utilising the imageListChangeCallback.
This callback fires whenever the image list changes, which means it also does whenever a user deletes an image.
However, by calling api.getImages(), you can get the list of user uploaded images and compare it to prior state, which enables you to filter for additions.
You can detect image deletion in a similar manner.
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
let printessApi = null;
let images = [];
const imageListChanged = () => {
const imageList = printessApi ? printessApi.api.getImages() : [];
if(imageList.length > images.length) {
window.alert("An image was uploaded!")
}
images = imageList;
}
printessApi = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Canvas",
templateVersion: "published",
imageListChangeCallback: () => imageListChanged(),
});
getImages<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
let printessApi = null;
let images = [];
const imageListChanged = () => {
const imageList = printessApi ? printessApi.api.getImages() : [];
if (imageList.length > images.length) {
window.alert("An image was uploaded!")
}
images = imageList;
}
printessApi = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Canvas",
templateVersion: "published",
imageListChangeCallback: () => imageListChanged(),
});
</script>
getImagesPrintess shows its own loading animation while the editor initialises and while it’s re-rendering the preview. If that doesn’t fit your shop’s design, you can suppress it with the attach parameter suppressLoadingAnimation: boolean and show your own overlay instead, driven by the loadingDoneCallback.
loadingDoneCallback fires once Printess has finished its initial load. Combine it with your own overlay element (marked printess-owned so Printess doesn’t hide it along with the rest of your page while the editor is in fullscreen) to give users a branded loading experience instead of - or on top of - Printess’ own spinner.
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Baby Photo Book",
templateVersion: "published",
suppressLoadingAnimation: true,
loadingDoneCallback: () => {
document.getElementById("my-overlay").style.display = "none";
}
});
<div id="my-overlay" class="printess-owned">
<div class="spinner"></div>
<p>Loading your design...</p>
</div>
<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Baby Photo Book",
templateVersion: "published",
basketId: "Some-Unique-Basket-Or-Session-Id",
suppressLoadingAnimation: true,
loadingDoneCallback: () => {
document.getElementById("my-overlay").style.display = "none";
},
addToBasketCallback: (saveToken, thumbnailUrl) => {
prompt("Savetoken:", saveToken)
}
})
</script>
#my-overlay {
position: absolute;
z-index: 99999999;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
}
If performance is a big concern of yours, you can accelerate the loading process of Printess through preloading.
For this you need to edit your webpage both in the <head> tag as well as the <body> tag.
The <head> needs to contain a link to our preload.js:
<link rel="preload" href="https://editor.printess.com/v/3.0.0/preload.js" as="script">
The body needs to add the preloaded scripts as a script tag:
<script>
const usedLaterScript = document.createElement("script");
// use EXACTLY the same url as in preload, otherwise it will be loaded in blocking mode!
usedLaterScript.src = "https://editor.printess.com/v/3.0.0/preload.js";
document.body.appendChild(usedLaterScript);
</script>
The URLs of the link inside your <head> and the source inside the <script> need to match exactly.
If your users want to use images that they uploaded earlier or in a different project, you can load up to 50 images through the API method importUserUploadedImages().
This method accepts up to two parameters, shopUserId and basketId, both being optional.
Note that user uploaded images are only kept for as long as the latest saveToken using them has been generated.
printess.api.importUserUploadedImages("unqiue-user-id-or-guid", "unique-per-session-guid");
If you call the method with both parameters, it returns images that belong both to the given userId as well as basketId (which can be different from your currently open editor session).
You may call the method with only the basketId, in which case only images uploaded to that session will be loaded.
Lastly, you may also call the method with only a userId, pulling from all images the user uploaded to any project.
Your user IDs need to be UNIQUE and NOT GUESSABLE, otherwise you risk your users data security.
Avoid plain integer IDs if you intend to use this feature.
<script type="module">
const shopAuthToken = "[your-shop-token]";
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: shopAuthToken,
templateName: "Canvas",
templateVersion: "draft",
shopUserId: "unqiue-user-id-or-guid",
basketId: "unique-per-session-guid-or-so"
});
/*
* This loads max 50 user images via shop user id.
* !!! USE A UNIQUE & NOT GUESSABLE USER ID, OTHERWISE THIS IS A USER DATA RISK !!!
*/
printess.api.importUserUploadedImages("unqiue-user-id-or-guid");
// This loads max 50 user images via shop user and basket id. Only uploaded images which belong to this user AND basket are returned.
//printess.api.importUserUploadedImages("unqiue-user-id-or-guid", "unique-per-session-guid-or-so");
</script>
importUserUploadedImagesIf you already know which image a buyer should start out with - for example an image they picked on a previous product page, or a photo coming from your own DAM/CMS - you don’t need to make them upload it manually. importImageFromUrl(url: string, assignToFrameOrNewFrame?: boolean, propertyId?: string): Promise<iExternalImage | null> downloads an image from a URL into Printess and can assign it directly to a frame.
The third parameter, propertyId, is what lets you target a specific frame instead of just the currently selected one: pass it in the form "frame:<jsName>", using the frame’s jsName as set up in the template.
const imgUrl = "https://your-shop.example.com/images/product-photo.jpg";
await printess.api.importImageFromUrl(imgUrl, true, "frame:image1");
importImageFromUrlThe jsName of a frame is set in the Printess Editor’s frame properties panel and is independent of the frame’s display name - it’s the identifier your code uses to address the frame, so it won’t break if someone renames the frame later.
<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "assign-image-to-named-frame",
templateVersion: "draft",
basketId: "Some-Unique-Basket-Or-Session-Id",
addToBasketCallback: (token, thumbnailUrl) => prompt("Savetoken:", token),
})
// Now upload and assign image to frame with jsName="image1"
const imgUrl = "https://resource.printess.com/uploads/fc8b773be98ee6d58ffebd9d955a55252ddc9a0a/images/a61ef5d34187cca9810714f4589d0c7a6a6725f6.jpg";
await printess.api.importImageFromUrl(imgUrl, true, "frame:image1")
</script>
importImageFromUrlYou can set up your templates with great functionalities using template scripts, and setting triggers to execute them throughout the template.
However, you can also execute them with the api function executeScript(scriptName: string, args: string[]): string | Promise<string>.
Note, that neither parameter of the function is optional, so if your script does not need any parameters, you will have to set the args to [], as we did in our example code:
const countThroughApi = () => {
if(printessApi) {
printessApi.api.executeScript("count", [])
}
}
executeScriptPrintess template scripts can have a return value, so naturally the executeScript forwards the return value of the script you execute, given the script does have one.
<script type="module">
const printessLoader = await import("https://editor.printess.com/v/nightly/printess-editor/loader.js");
const countThroughApi = () => {
if (printessApi) {
printessApi.api.executeScript("count", [])
}
}
/*
The "count" script in the template:
function count() {
const currentValue = form.number;
api.setFormFieldValue("number", currentValue + 1);
}
*/
const printessApi = await printessLoader.load({
token: "[your-shop-token]",
templateName: "template_script",
templateVersion: "published",
basketId: "my-unique-id",
buttons: [
{
label: "Count with API",
location: "button-bar",
clickCallback: countThroughApi,
icon: "docRef",
color: "secondary",
outline: "outline"
}
]
});
</script>
executeScriptUnlike the other examples on this page, this one doesn’t call the Printess JS API at all. It’s a small, standalone script you run once (in your browser console, or as a Node script) to generate text you then paste into a text frame inside the Printess Editor. It’s useful whenever you’re building a calendar template and don’t want to type out every date of every month by hand.
Printess text frames understand a small set of tags that lay out a calendar grid: <p:week> starts a new row (a week), and <w:weekday>, <w:weekend> and <w:other> each mark the start of a day cell - weekday and weekend style the cell for a day that belongs to the displayed month, while other is meant for the leading/trailing days that spill over from the previous or next month to fill out the grid. Each tag is immediately followed by the day number, and cells within a row are separated by tabs.
The script below computes those tags for any given month and year: it works out how many days the month has, what weekday the 1st falls on, and then fills a 6-row by 7-column grid, padding the start and end with the previous/next month’s trailing days (tagged <w:other>) so every week is a complete row.
const form = {
year: 2020,
month: 12,
};
function buildCalendarMarkup(year, month) {
const daysInMonth = new Date(year, month, 0).getDate();
const daysInMonthBefore = new Date(year, month - 1, 0).getDate();
const firstWeekday = new Date(year, month - 1, 1).getDay();
const startCol = firstWeekday === 0 ? 7 : firstWeekday; // Sunday = 0, shift so Monday = 1
const lines = [];
for (let row = 0; row < 6; row++) {
let line = '<p:week>';
for (let col = 1; col < 8; col++) {
const day = col + row * 7 - (startCol - 1);
if (day < 1) {
line += '\t<w:other>' + (daysInMonthBefore + day);
} else if (day > daysInMonth) {
line += '\t<w:other>' + (day - daysInMonth);
} else {
line += '\t<w:' + (col > 5 ? 'weekend' : 'weekday') + '>' + day;
}
}
lines.push(line);
}
return lines.join('');
}
console.log(buildCalendarMarkup(form.year, form.month));
Run this (for example by pasting it into your browser’s dev console), copy the logged output, and paste it directly into a text frame in the Printess Editor - the frame will render it as a calendar grid for the given month, using whichever character/paragraph styles you’ve set up for weekday, weekend and other day cells in that template.
const form = {
year: 2020,
month: 12,
};
console.log(`
Copy code after the line into a Printess text frame:
------------------------------------------------------------------
${(function (year, month) {
const daysInMonth = new Date(year, month, 0).getDate();
const daysInMonthBefore = new Date(year, month - 1, 0).getDate();
const firstWeekday = new Date(year, month - 1, 1).getDay();
const startCol = firstWeekday === 0 ? 7 : firstWeekday; // sunday = 0
const lines = [];
for (let row = 0; row < 6; row++) {
let line = '<p:week>';
for (let col = 1; col < 8; col++) {
const day = col + row * 7 - (startCol - 1);
if (day < 1) {
line += '\t<w:other>' + (daysInMonthBefore + day);
} else if (day > daysInMonth) {
line += '\t<w:other>' + (day - daysInMonth);
} else {
line += '\t<w:' + (col > 5 ? 'weekend' : 'weekday') + '>' + day;
}
}
lines.push(line);
}
return lines.join('');
})(form.year, form.month)}
--------------------------------------------------------------------
End of code to copy
`);
If you need to show a user additional information or take a complex action that cannot be done through the existing Printess UI, you can open a dialog and fill it with your own HTML.
You can achieve this through openDialog(options: IGenericDialogOptions): Promise<HTMLDivElement>.
The IGenericDialogOptions interface looks like this:
export interface IGenericDialogOptions {
callback: () => Promise<void | "keep-open"> | void | "keep-open",
// Called when the user clicks OK - return "keep-open"
// to keep the dialog open, e.g. after a failed validation
headline: string, // Headline for the dialog
okLabel?: string, // Custom label text for OK button
cancelLabel?: string, // Custom label text for cancel button
message?: string, // Message text for dialog
info?: string, // Additional info text
relativePosition?: boolean,
minHeight?: string,
cancelCallback?: () => Promise<void | "keep-open"> | void | "keep-open"
// Called when the user clicks Cancel - can also return "keep-open"
}
The function returns a <div> which is the inner element of the created dialog, and you can fill it with your own HTML by setting its innerHTML.
You can also apply inline styles to it, within certain boundaries.
If you just want to display a message for the user to acknowledge, simply use message.
Here you can see how to open a simple dialog with an input:
let email = "";
const onDialogButton = async () => {
const dialog = await printess.api.openDialog({
callback: () => console.log(email),
headline: "Account Data"
});
dialog.innerHTML = `
<div>
<label for="save-email">
Email:
</label>
<input name="save-email" type="text" id="save-email">
</div>
`
const input = dialog.querySelector("#save-email");
input.addEventListener("change", e => {
email = e.target.value;
});
}
openDialog<script type="module">
let email = "";
const onDialogButton = async () => {
const dialog = await printess.api.openDialog({
callback: () => console.log(email),
headline: "Account Data"
});
dialog.innerHTML = `
<div>
<label for="save-email">
Email:
</label>
<input name="save-email" type="text" id="save-email">
</div>
`
const input = dialog.querySelector("#save-email");
input.addEventListener("change", e => {
email = e.target.value;
});
}
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
const printess = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Canvas",
templateVersion: "published",
basketId: "Some-Unique-Basket-Or-Session-Id",
addToBasketCallback: (saveToken, thumbnailUrl) => {
console.log("saveToken", saveToken)
},
buttons: [
{
label: "Open Dialog",
location: "button-bar",
clickCallback: onDialogButton,
icon: "docRef",
color: "secondary",
outline: "outline"
}
],
})
</script>
openDialogBy setting up a few callbacks in your Printess load call, you can use Printess’ internal load and save logic, without having to store all the data yourself.
The Save & Load buttons only appear once getShopDataCallback, isShopUserLoggedInCallback, getShopLoginCallback and getShopSavedDataCallback are all set. getShopProjectDisplayNameCallback is additionally required when the user actually saves.
Users need to be able to log in to your shop in order to use this Save & Load system.
isShopUserLoggedInCallback is used by Printess do determine whether a user is logged in or not. If no user is logged in, it will show a Login button in the Save and Load dialogs and prohibit any other action. Otherwise the user will be able to save their work and load their previously saved projects.
getShopLoginCallback will be called when a user clicks the Login or Register button in Printess’ UI. It receives the clicked action ("register" | "login") along with the current saveToken, thumbnailUrl and project displayName, so you can complete the save after the user has logged in. Add the logic needed for your users to log in here.
getShopProjectDisplayNameCallback will be called when the user clicks the Save button in the Printess UI and the saving dialog appears.
getShopDataCallback is called when a user saves their current project and expects to receive any data from your shop system in the form of IShopData (see our TS documentation).
This data will be saved alongside the saved customisation state so any additional information that you need for your shop system should go here. This needs to include product information, so users can only save and load projects of the product they are currently editing.
If your shop system uses product options that change depending on Printess customisation (such as Form Fields), you should also save that information here, so you will have an easier time selecting the correct variants on loading the saved project.
getShopSavedDataCallback is called after a project has been successfully saved and confirms the shopData and display name that were stored.
shopDataLoadedCallback is called when a user loads a saved project and gives you all the data you had saved with the getShopDataCallback, plus the saveToken, displayName and thumbnailUrl. If the saved data contains any information on product variants for your shop system, you should read that info and select the according variants here.
If you want to implement saving & loading through this method, you should deactivate Saving and Loading buttons in your template options, otherwise there will be two sets of buttons confusing your customers.
<body>
<!-- By giving HTML elements the class "printess-owned", the editor is able to hide them when it is displayed itself and shows them again when it is hidden. -->
<!-- This way the Editor and surrounding HTML elements won't have to fight through z-indeces or other ugly methods. -->
<div class="printess-owned">
<h1> If you can read this, the Editor is hidden </h1>
<button> Show Editor </button>
</div>
</body>
<script type="module">
const printessLoader = await import("https://editor.printess.com/printess-editor/loader.js");
let savedState = "";
let loggedIn = false;
const userId = "uniqueUserId";
// product is of type IProduct (see printess-editor.d.ts)
const product = {
id: "1",
}
// shopData is of type IShopData (see printess-editor.d.ts)
const shopData = {
shopId: "myShop",
shopUserId: userId,
product: product,
}
const onBack = (saveToken, thumbnailUrl) => {
savedState = saveToken;
if (printessApi) {
printessApi.ui.hide();
}
}
const onBasket = (saveToken, thumbnailUrl) => {
alert(`Cast it in the Basket! \nSave Token: \n${saveToken} \nThumbnail \n${thumbnailUrl}`);
try {
// Call your backend to send the saveToken to production and close the editor
ui?.hide()
} catch (err) {
alert(err.message);
}
}
const printessApi = await printessLoader.load({
token: "[your-shop-token]",
templateName: "Phone-Case",
templateVersion: "published",
backButtonCallback: onBack,
addToBasketCallback: onBasket,
isShopUserLoggedInCallback: () => {
return loggedIn;
},
getShopDataCallback: () => {
return shopData;
},
shopLoginCallback: () => {
loggedIn = true;
},
getShopSavedDataCallback: (savedData) => {
alert("getShopSavedDataCallback", savedData);
},
getShopProjectDisplayNameCallback: () => {
// you could create your own project naming dialog here
}
});
const showButton = document.querySelector("button");
showButton.addEventListener("click", () => {
printessApi.api.load(savedState);
printessApi.ui.show();
})
</script>
loadThe Photobook comes with a variety of settings which are unique to it, which is why we gave it its own interface to set them: adjustBook().
This call receives an iExternalBookSettings object, which looks like this:
export type iExternalBookSettings = {
/** optional: could be any Length value, like an equation or a fixed value with unit, e.g. `=spine.pages * 0.3mm` or `2cm` */
spine?: string,
/** optional: `hinge` could be a Length value, like `1cm` or `2inch` or a number in pixel */
hinge?: number | string,
/** optional: `edge-left-right` could be a Length value, like `1cm` or `2inch` or a number in pixel */
edgeX?: number | string,
/** optional: `edge-top-bottom` could be a Length value, like `1cm` or `2inch` or a number in pixel */
edgeY?: number | string,
/** optional: `bleed-left-right` could be a Length value, like `1cm` or `2inch` or a number in pixel */
bleedX?: number | string,
/** optional: `bleed-top-bottom` could be a Length value, like `1cm` or `2inch` or a number in pixel */
bleedY?: number | string,
/** optional: `Minimum Book Pages` set min pages value and auto adds additional pages */
minPages?: number,
/** optional: `Maximum Book Pages` set max pages and outo removes overidge pages */
maxPages?: number,
/** optional: `Initial Freestyle Photobook Pages` set initial amount of pages the freestyle photobook is created with */
initialFreestylePhotobookPages?: number,
/** optinal: enable / disable layflat mode */
layflat?: boolean,
/** optional: if true, first page and last page become invisible */
lockCoverInside?: boolean
/** optional: determines the min-spreads to add and also if the spread-count needs to be divisible by 2 to be printed */
addSpreads?: 1 | 2
/** optional: set the book inside pages document imposition by name */
bookImposition?: string,
/** optional: set all cover documents imposition by name */
coverImposition?: string
previewCoverType?: "hard" | "soft";
/** optional: enable the debossed (premade) cover - the cover gets a fixed number of images and no theme cover layout is applied */
useDebossedCover?: boolean,
/** optional: number of images (0-4) placed on the debossed cover */
debossedCoverImageCount?: number,
}
You can also adjust the Photobook settings on load by using the attach parameter bookSettings.
It also accepts an iExternalBookSettings object.
You can set the book’s spine width either in a total value or using a formular.
If you use a formular that depends on other settings, such as form fields (e.g. PAGE_COUNT), you need to make sure to call adjustBook() whenever one of the form fields changes.
You can set the length of your books hinge, for example when your cover material changes and you need to adjust the books hinge to accomodate the material.
You can set outside edges of spreads through edgeX (horizontal) and edgeY (vertical).
A change in e.g. material might lead to different amounts of page limits, which you can adjust using minPages and maxPages.
If you increase the value for minPages, it makes sense to check the current PAGE_COUNT and if it is lower than the new minimum, set it to the new value in the same action.
This prevents the user from having to add pages and spares them a warning that does not need their input to be fixed.
The option initialFreestylePhotobookPages only applies when the bookSettings are used for initial loading through the attach parameter.
It is also only applicable to Freestyle editing, because it circumvents some of the Magic in Magic Photobooks, namely adjusting the page count dynamically depending on the number of images that were provided by the user.
It sets the initial page count of the photobook, overriding your photobook theme setting.
This setting will remove the single pages from the Inside Pages Document - for example if it will be produced with a flush binding.
You can get animations you created as HTML to easily integrate them into your website through the getAnimationHtmlAsString() method, which responds with an object containing pxWidth, pxHeight and data, the latter being the HTML string.
As you can see in our example linked below, you can then use this as the source for an HTML element.
const r = await printess.api.getAnimationHtmlAsString();
if (r) {
const iFrame = document.createElement("iframe");
console.log("getAnimationHtmlAsString:", r);
iFrame.srcdoc = r.data;
iFrame.classList.add("printess-owned");
iFrame.setAttribute("sandbox", "allow-scripts allow-popups allow-same-origin allow-presentation");
iFrame.setAttribute("style", `
position: absolute;
z-index: 99999999;
left: 50%;
top: 100px;
width: ${r.pxWidth}px;
height: ${r.pxHeight}px;
background: white;
box-shadow: black 2px 2px 10px;
border: 20px solid #ccc;
box-sizing: content-box;
transform: translate(-50%, 0);
`);
}
getAnimationHtmlAsString