Skip to content

Commit

Permalink
Feature: Flaticon integration (#21)
Browse files Browse the repository at this point in the history
* Add html elements for icon search

* Add css class for icon search button

* Add css class to select icon results

* Add function to request flaticon api

* Display preview of flaticon request

* Add advice for debugging to README

* Add icon result filter

* Add filter parameters to freepik api request

* Add object with id for freepik api response

* Add function to get download path of clicked icon

* Add function to download clicked icon

* Add function to retrieve base64 string of clicked icon

* Add function to insert icon to slide

* Move icon download functions to separate file

* Refactor base64 image insertion function

* Refactor method name for adding icon previews

* Extract methods for icon search integration

* Display icon previews in dropdown

* pair programming

* Refactor variable and element names

---------

Co-authored-by: Kyrylo Zakurdaiev <[email protected]>
  • Loading branch information
fabiankuenzer and kerilz authored Jul 11, 2024
1 parent 379091c commit d5d9f5a
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ command:
npm run stop
```

## Debugging
After running the following command, the developer console is available with right click into the add-in window and selecting element information.
`defaults write com.microsoft.Powerpoint OfficeWebAddinDeveloperExtras -bool true`

⚠️ Mac App Store builds of Office do not support the OfficeWebAddinDeveloperExtras flag.

## Helpful links

[PowerPoint Api-Documentation](https://learn.microsoft.com/de-de/javascript/api/powerpoint?view=powerpoint-js-preview)
Expand Down
55 changes: 55 additions & 0 deletions src/taskpane/iconDownloadUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FetchIconResponse } from "./types";

export async function fetchIcons(searchTerm: string): Promise<Array<FetchIconResponse>> {
const url = `https://hammerhead-app-fj5ps.ondigitalocean.app/icons?term=${searchTerm}&family-id=300&filters[shape]=outline&filters[color]=solid-black&filters[free_svg]=premium`;
const requestHeaders = new Headers();
requestHeaders.append("X-Freepik-API-Key", "FPSX6fb1f23cbea7497387b5e5b8eb8943de");
const requestOptions = {
method: "GET",
headers: requestHeaders,
};

try {
const result = await fetch(url, requestOptions);
const response = await result.json();
return response.data
.filter((obj) => obj.author.name === "Smashicons" && obj.family.name === "Basic Miscellany Lineal")
.map((obj) => ({
id: obj.id.toString(),
url: obj.thumbnails[0].url,
}))
.slice(0, 50);
} catch (e) {
throw new Error("Error fetching icons: " + e);
}
}

export async function getDownloadPathForIconWith(id: string) {
const url = `https://hammerhead-app-fj5ps.ondigitalocean.app/icons/${id}/download?format=png`;
const requestHeaders = new Headers();
requestHeaders.append("X-Freepik-API-Key", "FPSX6fb1f23cbea7497387b5e5b8eb8943de");
const requestOptions = {
method: "GET",
headers: requestHeaders,
};

try {
const result = await fetch(url, requestOptions);
const response = await result.json();
return response.data.url;
} catch (e) {
throw new Error("Error getting download url: " + e);
}
}

export async function downloadIconWith(url: string) {
const requestOptions = {
method: "GET",
};

try {
return await fetch(url, requestOptions);
} catch (e) {
throw new Error("Error downloading icon: " + e);
}
}
8 changes: 7 additions & 1 deletion src/taskpane/taskpane.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ ul {
width: 31%;
}

.icon-search-button {
width: 31%;
}

.icon-results {}

.grid-button {
width: 49%
}
Expand All @@ -164,4 +170,4 @@ ul {

.dropdown-menu.show {
display: block;
}
}
7 changes: 7 additions & 0 deletions src/taskpane/taskpane.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ <h3 class="ms-font-l">L21's-Logo</h3>
</ul>
</div>

<h3 class="ms-font-l">Icons</h3>
<input class="icon-search-button" type="text" id="icon-search-input" />
<div class="content">
<button class="dropdown-trigger btn" data-target="icon-previews" id="icons">Search</button>
<ul id="icon-previews" class="dropdown-content"></ul>
</div>

</div>
</section>
<script type="text/javascript" src="../../lib/materialize/js/materialize.min.js"></script>
Expand Down
105 changes: 103 additions & 2 deletions src/taskpane/taskpane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { base64Images } from "../../base64Image";
import * as M from "../../lib/materialize/js/materialize.min";
import { runPowerPoint } from "./powerPointUtil";
import { columnLineName, rowLineName, createColumns, createRows } from "./rowsColumns";
import { getDownloadPathForIconWith, downloadIconWith, fetchIcons } from "./iconDownloadUtils";
import { FetchIconResponse } from "./types";

Office.onReady((info) => {
if (info.host === Office.HostType.PowerPoint) {
Expand All @@ -29,6 +31,10 @@ Office.onReady((info) => {
document.querySelectorAll(".logo-button").forEach((button) => {
(button as HTMLElement).onclick = () => insertImageByBase64(button.getAttribute("data-value"));
});

initDropdownPlaceholder();
addIconSearch();
insertIconOnClickOnPreview();
}
});

Expand All @@ -39,7 +45,7 @@ function initRowsAndColumnsButtons() {
document.querySelectorAll(".row-button").forEach((button) => {
(button as HTMLElement).onclick = () => {
createRows(Number(button.getAttribute("data-value")));
}
};
});

document.querySelectorAll(".column-button").forEach((button) => {
Expand Down Expand Up @@ -96,7 +102,7 @@ export async function insertSticker(color) {
const shapes = powerPointContext.presentation.getSelectedSlides().getItemAt(0).shapes;
const textBox = shapes.addTextBox(
localStorage.getItem("initials") + ", " + today.toDateString() + "\n",
{ height: 50, left: 50, top: 50, width: 150 }
{ height: 50, left: 50, top: 50, width: 150 }
);
textBox.name = "Square";
textBox.fill.setSolidColor(rgbToHex(color));
Expand All @@ -107,6 +113,7 @@ export async function insertSticker(color) {
function rgbToHex(rgb: String) {
const regex = /(\d+),\s*(\d+),\s*(\d+)/;
const matches = rgb.match(regex);

function componentToHex(c: String) {
const hex = Number(c).toString(16);
return hex.length === 1 ? "0" + hex : hex;
Expand All @@ -132,3 +139,97 @@ export async function addBackground(backgroundColor?: string) {
selectedImage.fill.setSolidColor(backgroundColor);
});
}

function addIconPreviewWith(icons: FetchIconResponse[]) {
for (let i = 0; i < icons.length; i += 5) {
const iconPreviewElement = document.getElementById("icon-previews");
const listElement = document.createElement("li");
const anchorElement = document.createElement("a");
iconPreviewElement.appendChild(listElement);
listElement.appendChild(anchorElement);

icons.slice(i, i + 5).forEach((icon) => {
const iconPreviewElement = document.createElement("img");
iconPreviewElement.id = icon.id;
iconPreviewElement.src = icon.url;
iconPreviewElement.width = 45;
iconPreviewElement.height = 45;
anchorElement.appendChild(iconPreviewElement);
});
}
}

async function insertBase64ImageOn(event) {
const imageSizeInPixels = 500;
const path = await getDownloadPathForIconWith(event.target.id);
let base64Image: string = await downloadIconWith(path)
.then((response) => response.blob())
.then(
(blob) =>
new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = () => {
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = imageSizeInPixels;
canvas.height = imageSizeInPixels;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, imageSizeInPixels, imageSizeInPixels);
resolve(canvas.toDataURL("image/png"));
};
img.onerror = reject;
img.src = reader.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(blob);
})
);

if (base64Image.startsWith("data:")) {
const PREFIX = "base64,";
const base64Idx = base64Image.indexOf(PREFIX);
base64Image = base64Image.substring(base64Idx + PREFIX.length);
}

Office.context.document.setSelectedDataAsync(
base64Image,
{ coercionType: Office.CoercionType.Image },
(asyncResult) => {
if (asyncResult.status === Office.AsyncResultStatus.Failed) {
console.error(`Insert image failed. Code: ${asyncResult.error.code}. Message: ${asyncResult.error.message}`);
}
}
);
}

function addIconSearch() {
document.getElementById("icons").onclick = async () => {
document.querySelectorAll("#icon-previews li").forEach((li) => li.remove());

try {
const searchTerm = (<HTMLInputElement>document.getElementById("icon-search-input")).value;
const result = await fetchIcons(searchTerm);
addIconPreviewWith(result);
} catch (e) {
throw new Error("Error retrieving icon urls: " + e);
}
};
}

function insertIconOnClickOnPreview() {
document.getElementById("icon-previews").addEventListener("click", (event) => insertBase64ImageOn(event), false);
}

function initDropdownPlaceholder() {
const iconPreviewElement = document.getElementById("icon-previews");
for (let i = 0; i < 15; i++) {
const spanElement = document.createElement("span");
spanElement.innerText = "Loading...";
const anchorElement = document.createElement("a");
const listElement = document.createElement("li");
iconPreviewElement.appendChild(listElement);
listElement.appendChild(anchorElement);
anchorElement.appendChild(spanElement);
}
}
4 changes: 4 additions & 0 deletions src/taskpane/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type FetchIconResponse = {
id: string;
url: string;
};

0 comments on commit d5d9f5a

Please sign in to comment.