I have a content script that injects an iframe into a webpage.
content.js
var iframe = document.createElement('iframe');
iframe.id = "frame";
iframe.style.cssText = "position:fixed;top: 15px;right: 15px;width: 250px;height: 245px;overflow: hidden;background-color:#FFFFFF;border-radius: 5px;";
iframe.src = chrome.runtime.getURL('frame.html');
document.body.appendChild(iframe);
The iframe displays some text values, has a submit and a close button.
part of frame.html
<div class="header">
<span class="close">Name</span>
<span class="close-btn" id="close-btn">×</span>
</div>
<div class="details-container">
<span class="label">First Name : </span>
<span id="fname" type="text" ></span>
</div>
<div class="details-container">
<span class="label">Last Name : </span>
<span id="lname" type="text" /></span>
</div>
<div class="btn-details-container">
<button class="copy" id="copy-name">Copy</button>
</div>
frame.html has frame.js linked to it.
I want to do 2 things here.
Close/Remove/Hide the iframe when user clicks on close button on the iframe(#close-btn)
The values of first name and last name in span to be dynamically set (extracted from DOM of current webpage)
Problems:
1)I don't know how to propogate click event on frame.html to content script to close iframe(Unable to establish communication between frame.js and content.js)
2)Not able to set span.textContent for #fname and #lname because frame.js is not able to read webpage DOM.
Extension messaging (iframe controls the logic)
Use chrome.tabs.sendMessage to communicate with the owner tab of the iframe, which can be retrieved using chrome.tabs.getCurrent inside the iframe.
content.js:
var FRAME_URL = chrome.runtime.getURL('frame.html');
var iframe = document.createElement('iframe');
iframe.src = FRAME_URL;
document.body.appendChild(iframe);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.cmd) {
case 'close':
iframe.remove();
iframe = null;
break;
case 'getData':
sendResponse([
['fname', document.querySelector('.web.page.selector.for.fname').textContent],
['lname', document.querySelector('.web.page.selector.for.lname').textContent],
]);
break;
}
});
iframe.js:
tellParent({cmd: 'getData'}, data => {
for (const [id, val] of data) {
document.getElementById(id).textContent = val;
}
});
document.querySelector('.close-btn').onclick = () => {
tellParent({cmd: 'close'});
};
function tellParent(msg, callback) {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, msg, {frameId: 0}, callback);
});
}
Extension messaging (two-way port)
Initiate the port using chrome.tabs.connect in the iframe, then use it in the content script.
content script:
let framePort;
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'frame') {
// global framePort can be used by code that will run in the future
framePort = port;
port.postMessage({foo: 'bar'});
}
});
// add iframe element and point it to chrome.runtime.getURL('iframe.html')
//...........
iframe script:
chrome.tabs.getCurrent(tab => {
const port = chrome.tabs.connect(tab.id, {name: 'frame', frameId: 0});
port.onMessage.addListener(msg => {
if (msg.foo === 'bar') {
console.log(msg);
}
});
});
Web messaging (two-way MessagePort)
It's super fast and supports binary data types like Blob or ArrayBuffer but requires certain care to avoid interception by the web page:
Create the iframe inside a closed ShadowDOM to avoid exposing window[0]
Don't set iframe's src, instead navigate its inner location using a random secret in the url parameters so that its URL won't be spoofed by the web page or other extensions which used chrome.dom.openOrClosedShadowRoot.
pass the safe MessagePort into the iframe via postMessage
use this safe MessagePort for two-way communication
// content.js
(async () => {
const port = await makeExtensionFramePort('/iframe.html');
port.onmessage = e => {
console.log('from iframe:', e.data);
};
port.postMessage(123);
port.postMessage({ foo: bar });
port.postMessage(new Blob(['foo']));
})();
async function makeExtensionFramePort(path) {
const secret = Math.random().toString(36);
const url = new URL(chrome.runtime.getURL(path));
url.searchParams.set('secret', secret);
const el = document.createElement('div');
const root = el.attachShadow({mode: 'closed'});
const iframe = document.createElement('iframe');
iframe.hidden = true;
root.appendChild(iframe);
(document.body || document.documentElement).appendChild(el);
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
iframe.contentWindow.location.href = url;
});
const mc = new MessageChannel();
iframe.contentWindow.postMessage(1, '*', [mc.port2]);
return mc.port1;
}
// iframe.html:
<script src="iframe.js"></script>
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
port = e.ports[0];
port.onmessage = onContentMessage;
}
};
function onContentMessage(e) {
console.log('from content:', e.data);
port.postMessage('ok');
}
Modification: a direct two-way port between the content script and the extension's service worker by using navigator.serviceWorker messaging in the iframe:
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
navigator.serviceWorker.ready.then(swr => {
swr.active.postMessage('port', [e.ports[0]]);
});
}
};
// background.js
self.onmessage = e => {
if (e.data === 'port') {
e.ports[0].onmessage = onContentMessage;
}
}
function onContentMessage(e) {
// prints both in the background console and in the iframe's console
console.log('from content:', e.data);
port.postMessage('ok');
}
Related
I have a content script that injects an iframe into a webpage.
content.js
var iframe = document.createElement('iframe');
iframe.id = "frame";
iframe.style.cssText = "position:fixed;top: 15px;right: 15px;width: 250px;height: 245px;overflow: hidden;background-color:#FFFFFF;border-radius: 5px;";
iframe.src = chrome.runtime.getURL('frame.html');
document.body.appendChild(iframe);
The iframe displays some text values, has a submit and a close button.
part of frame.html
<div class="header">
<span class="close">Name</span>
<span class="close-btn" id="close-btn">×</span>
</div>
<div class="details-container">
<span class="label">First Name : </span>
<span id="fname" type="text" ></span>
</div>
<div class="details-container">
<span class="label">Last Name : </span>
<span id="lname" type="text" /></span>
</div>
<div class="btn-details-container">
<button class="copy" id="copy-name">Copy</button>
</div>
frame.html has frame.js linked to it.
I want to do 2 things here.
Close/Remove/Hide the iframe when user clicks on close button on the iframe(#close-btn)
The values of first name and last name in span to be dynamically set (extracted from DOM of current webpage)
Problems:
1)I don't know how to propogate click event on frame.html to content script to close iframe(Unable to establish communication between frame.js and content.js)
2)Not able to set span.textContent for #fname and #lname because frame.js is not able to read webpage DOM.
Extension messaging (iframe controls the logic)
Use chrome.tabs.sendMessage to communicate with the owner tab of the iframe, which can be retrieved using chrome.tabs.getCurrent inside the iframe.
content.js:
var FRAME_URL = chrome.runtime.getURL('frame.html');
var iframe = document.createElement('iframe');
iframe.src = FRAME_URL;
document.body.appendChild(iframe);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.cmd) {
case 'close':
iframe.remove();
iframe = null;
break;
case 'getData':
sendResponse([
['fname', document.querySelector('.web.page.selector.for.fname').textContent],
['lname', document.querySelector('.web.page.selector.for.lname').textContent],
]);
break;
}
});
iframe.js:
tellParent({cmd: 'getData'}, data => {
for (const [id, val] of data) {
document.getElementById(id).textContent = val;
}
});
document.querySelector('.close-btn').onclick = () => {
tellParent({cmd: 'close'});
};
function tellParent(msg, callback) {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, msg, {frameId: 0}, callback);
});
}
Extension messaging (two-way port)
Initiate the port using chrome.tabs.connect in the iframe, then use it in the content script.
content script:
let framePort;
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'frame') {
// global framePort can be used by code that will run in the future
framePort = port;
port.postMessage({foo: 'bar'});
}
});
// add iframe element and point it to chrome.runtime.getURL('iframe.html')
//...........
iframe script:
chrome.tabs.getCurrent(tab => {
const port = chrome.tabs.connect(tab.id, {name: 'frame', frameId: 0});
port.onMessage.addListener(msg => {
if (msg.foo === 'bar') {
console.log(msg);
}
});
});
Web messaging (two-way MessagePort)
It's super fast and supports binary data types like Blob or ArrayBuffer but requires certain care to avoid interception by the web page:
Create the iframe inside a closed ShadowDOM to avoid exposing window[0]
Don't set iframe's src, instead navigate its inner location using a random secret in the url parameters so that its URL won't be spoofed by the web page or other extensions which used chrome.dom.openOrClosedShadowRoot.
pass the safe MessagePort into the iframe via postMessage
use this safe MessagePort for two-way communication
// content.js
(async () => {
const port = await makeExtensionFramePort('/iframe.html');
port.onmessage = e => {
console.log('from iframe:', e.data);
};
port.postMessage(123);
port.postMessage({ foo: bar });
port.postMessage(new Blob(['foo']));
})();
async function makeExtensionFramePort(path) {
const secret = Math.random().toString(36);
const url = new URL(chrome.runtime.getURL(path));
url.searchParams.set('secret', secret);
const el = document.createElement('div');
const root = el.attachShadow({mode: 'closed'});
const iframe = document.createElement('iframe');
iframe.hidden = true;
root.appendChild(iframe);
(document.body || document.documentElement).appendChild(el);
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
iframe.contentWindow.location.href = url;
});
const mc = new MessageChannel();
iframe.contentWindow.postMessage(1, '*', [mc.port2]);
return mc.port1;
}
// iframe.html:
<script src="iframe.js"></script>
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
port = e.ports[0];
port.onmessage = onContentMessage;
}
};
function onContentMessage(e) {
console.log('from content:', e.data);
port.postMessage('ok');
}
Modification: a direct two-way port between the content script and the extension's service worker by using navigator.serviceWorker messaging in the iframe:
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
navigator.serviceWorker.ready.then(swr => {
swr.active.postMessage('port', [e.ports[0]]);
});
}
};
// background.js
self.onmessage = e => {
if (e.data === 'port') {
e.ports[0].onmessage = onContentMessage;
}
}
function onContentMessage(e) {
// prints both in the background console and in the iframe's console
console.log('from content:', e.data);
port.postMessage('ok');
}
I have a content script that injects an iframe into a webpage.
content.js
var iframe = document.createElement('iframe');
iframe.id = "frame";
iframe.style.cssText = "position:fixed;top: 15px;right: 15px;width: 250px;height: 245px;overflow: hidden;background-color:#FFFFFF;border-radius: 5px;";
iframe.src = chrome.runtime.getURL('frame.html');
document.body.appendChild(iframe);
The iframe displays some text values, has a submit and a close button.
part of frame.html
<div class="header">
<span class="close">Name</span>
<span class="close-btn" id="close-btn">×</span>
</div>
<div class="details-container">
<span class="label">First Name : </span>
<span id="fname" type="text" ></span>
</div>
<div class="details-container">
<span class="label">Last Name : </span>
<span id="lname" type="text" /></span>
</div>
<div class="btn-details-container">
<button class="copy" id="copy-name">Copy</button>
</div>
frame.html has frame.js linked to it.
I want to do 2 things here.
Close/Remove/Hide the iframe when user clicks on close button on the iframe(#close-btn)
The values of first name and last name in span to be dynamically set (extracted from DOM of current webpage)
Problems:
1)I don't know how to propogate click event on frame.html to content script to close iframe(Unable to establish communication between frame.js and content.js)
2)Not able to set span.textContent for #fname and #lname because frame.js is not able to read webpage DOM.
Extension messaging (iframe controls the logic)
Use chrome.tabs.sendMessage to communicate with the owner tab of the iframe, which can be retrieved using chrome.tabs.getCurrent inside the iframe.
content.js:
var FRAME_URL = chrome.runtime.getURL('frame.html');
var iframe = document.createElement('iframe');
iframe.src = FRAME_URL;
document.body.appendChild(iframe);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.cmd) {
case 'close':
iframe.remove();
iframe = null;
break;
case 'getData':
sendResponse([
['fname', document.querySelector('.web.page.selector.for.fname').textContent],
['lname', document.querySelector('.web.page.selector.for.lname').textContent],
]);
break;
}
});
iframe.js:
tellParent({cmd: 'getData'}, data => {
for (const [id, val] of data) {
document.getElementById(id).textContent = val;
}
});
document.querySelector('.close-btn').onclick = () => {
tellParent({cmd: 'close'});
};
function tellParent(msg, callback) {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, msg, {frameId: 0}, callback);
});
}
Extension messaging (two-way port)
Initiate the port using chrome.tabs.connect in the iframe, then use it in the content script.
content script:
let framePort;
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'frame') {
// global framePort can be used by code that will run in the future
framePort = port;
port.postMessage({foo: 'bar'});
}
});
// add iframe element and point it to chrome.runtime.getURL('iframe.html')
//...........
iframe script:
chrome.tabs.getCurrent(tab => {
const port = chrome.tabs.connect(tab.id, {name: 'frame', frameId: 0});
port.onMessage.addListener(msg => {
if (msg.foo === 'bar') {
console.log(msg);
}
});
});
Web messaging (two-way MessagePort)
It's super fast and supports binary data types like Blob or ArrayBuffer but requires certain care to avoid interception by the web page:
Create the iframe inside a closed ShadowDOM to avoid exposing window[0]
Don't set iframe's src, instead navigate its inner location using a random secret in the url parameters so that its URL won't be spoofed by the web page or other extensions which used chrome.dom.openOrClosedShadowRoot.
pass the safe MessagePort into the iframe via postMessage
use this safe MessagePort for two-way communication
// content.js
(async () => {
const port = await makeExtensionFramePort('/iframe.html');
port.onmessage = e => {
console.log('from iframe:', e.data);
};
port.postMessage(123);
port.postMessage({ foo: bar });
port.postMessage(new Blob(['foo']));
})();
async function makeExtensionFramePort(path) {
const secret = Math.random().toString(36);
const url = new URL(chrome.runtime.getURL(path));
url.searchParams.set('secret', secret);
const el = document.createElement('div');
const root = el.attachShadow({mode: 'closed'});
const iframe = document.createElement('iframe');
iframe.hidden = true;
root.appendChild(iframe);
(document.body || document.documentElement).appendChild(el);
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
iframe.contentWindow.location.href = url;
});
const mc = new MessageChannel();
iframe.contentWindow.postMessage(1, '*', [mc.port2]);
return mc.port1;
}
// iframe.html:
<script src="iframe.js"></script>
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
port = e.ports[0];
port.onmessage = onContentMessage;
}
};
function onContentMessage(e) {
console.log('from content:', e.data);
port.postMessage('ok');
}
Modification: a direct two-way port between the content script and the extension's service worker by using navigator.serviceWorker messaging in the iframe:
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
navigator.serviceWorker.ready.then(swr => {
swr.active.postMessage('port', [e.ports[0]]);
});
}
};
// background.js
self.onmessage = e => {
if (e.data === 'port') {
e.ports[0].onmessage = onContentMessage;
}
}
function onContentMessage(e) {
// prints both in the background console and in the iframe's console
console.log('from content:', e.data);
port.postMessage('ok');
}
I'm messing around w/ Office Dialog for Add-Ins in JS. I've got it so far where I can open a dialog, capture input to console, run a function from a button and close the dialog box, but I can't seem to get my function to interact with Excel. It's lost context I beleive, I tried using this and I get no errors, but it doesn't work --> var context = new Excel.RequestContext().
Here is my open function and my main function and the end function.
main.js
export async function helloworld(event) {
try {
await Excel.run(async (context) => {
//Start Func
console.log("BEFORE OPEN UI");
openDialog("/yo/dist/dialog.html", 30, 20);
console.log("AFTER OPEN UI");
await context.sync()
.then(function () {
console.log("AFTER SYNC UI");
var ws = context.workbook.worksheets.getActiveWorksheet();
var range = ws.getRange("A1:D5");
range.select();
})
//End Func
await context.sync();
});
} catch (error) {
console.error(error);
}
console.log("EVENT COMPLETEED HELLOW");
//event.completed();
}
open.js
function openDialog(RelURLStr, H, W) {
Office.context.ui.displayDialogAsync(window.location.origin + RelURLStr,
{ height: H, width: W }, dialogCallback);
}
run func //this gets ran, but nothing output to worksheet and no errors.
function dobuttonrun(event) {
console.log("ENDING");
var context = new Excel.RequestContext()
var ws = context.workbook.worksheets.getActiveWorksheet();
var fakedatarng = ws.getRange("A1");
fakedatarng.values = "TEST";
return context.sync();
event.completed();
}
function getGlobal() {
return typeof self !== "undefined"
? self
: typeof window !== "undefined"
? window
: typeof global !== "undefined"
? global
: undefined;
}
const g = getGlobal();
// The add-in command functions need to be available in global scope
g.dobuttonrun = dobuttonrun;
Opening a popup has UX issues on web clients. If you need to support excel web, it will ask the user a confirmation for the dialog each time and cannot by bypassed; Excel is asking not the browser!
I strongly suggest you, to use a Popup from your UX framework. If you are using Fluent UI, the microsoft suggested framework that keeps look and feel of office please refer to this page.
In case that you need a dialog solution, keep in mind that you are running a new page /yo/dist/dialog.html you'll loose all the variables and context from your parent webpage:
If you are using any SPA javascript framework like react.js or VUE.js, another clean app will be rendered.
BTW You can achieve your need implementing a simple communication as follows:
// Office.Dialog type
let dialog;
export async function helloworld(event) {
Office.context.ui.displayDialogAsync(
'/yo/dist/dialog',
{ promptBeforeOpen: true, height: 70, width: 50, displayInIframe: false },
(result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
// unable to manage status
throw new Error(result.error.message)
}
else {
dialog = result.value;
dialog.addEventHandler(Office.EventType.DialogMessageReceived, messageHandler);
}
}
)
}
/**
* Assign the project to an employee.
* #param {Object} arg - Event handler args
* #param {string} arg.message - Message as string (NOT js OBJECT)
* #param {string} arg.origin - Message Origin
*/
const messageHandler = (arg) => {
// messaging can send only strings.
// If you need to pass complex JSON you need to stringify and parse objects
const message = JSON.parse(arg.message)
if (message.type === "execution-canceled") {
// you have all props of message like message.reason
dialog.close();
return
}
if (message.type === "execution-confirmed") {
// execute than close popup
// if you need to close popup than execute, just run dialog.close()
// before doStuff() and remove finally method call
doStuff()
.then(() => console.log("EVENT COMPLETEED HELLOW"))
.catch(error => console.error(error))
.finally(() => dialog.close())
}
}
const doStuff = async () => Excel.run(async (context) => {
// you don't need context.sync() before interacting excel
var ws = context.workbook.worksheets.getActiveWorksheet();
var range = ws.getRange("A1:D5");
range.select();
await context.sync();
})
On your dialog page you need to wait for Office.initialize in order to use Office.context.ui.messaging apis, than you can simply send your object.
const send = (message) => {
Office.context.ui.messageParent(JSON.stringify(message));
}
Office.initialize = () => {
// render page or activate button
}
const onExecutionConfirmed = () => send({ type: 'execution-confirmed' })
const onExcecutionCanceled = () => send({
type: 'execution-canceled',
reason: "additional infos can be placed in any props of this serialized JSON object"
})
Here is what I finished with after #CLAudio detailed answer.
commands.js
Office.onReady(() => {
// If needed, Office.js is ready to be called
});
export async function helloworld(event) {
try {
await Excel.run(async (context) => {
//Start Func
openDialog("/yo/dist/dialog.html", 30, 20);
//End Func
await context.sync();
});
} catch (error) {
console.error(error);
}
event.completed();
}
const doStuff = async () => Excel.run(async (context) => {
// you don't need context.sync() before interacting excel
var ws = context.workbook.worksheets.getActiveWorksheet();
var range = ws.getRange("A1:D5");
range.select();
await context.sync();
})
//////////////////////////////////////////
let dialog;
function openDialog(HTMLUrl, H, W) {
Office.context.ui.displayDialogAsync(window.location.origin + HTMLUrl, { promptBeforeOpen: true, height: H, width: W, displayInIframe: false },
(asyncResult) => {
if (asyncResult.status === Office.AsyncResultStatus.Failed) {
// In addition to general system errors, there are 3 specific errors for
// displayDialogAsync that you can handle individually.
switch (asyncResult.error.code) {
case 12004:
console.log("Domain is not trusted");
break;
case 12005:
console.log("HTTPS is required");
break;
case 12007:
console.log("A dialog is already opened.");
break;
default:
console.log(asyncResult.error.message);
break;
}
} else {
dialog = asyncResult.value;
/*Messages are sent by developers programatically from the dialog using office.context.ui.messageParent(...)*/
dialog.addEventHandler(Office.EventType.DialogMessageReceived, messageHandler);
/*Events are sent by the platform in response to user actions or errors. For example, the dialog is closed via the 'x' button*/
dialog.addEventHandler(Office.EventType.DialogEventReceived, eventHandler);
}
}
)
}
function eventHandler(arg) {
// In addition to general system errors, there are 2 specific errors
// and one event that you can handle individually.
switch (arg.error) {
case 12002:
console.log("Cannot load URL, no such page or bad URL syntax.");
break;
case 12003:
console.log("HTTPS is required.");
break;
case 12006:
// The dialog was closed, typically because the user the pressed X button.
console.log("Dialog closed by user");
break;
default:
console.log("Undefined error in dialog window");
break;
}
}
/**
* Assign the project to an employee.
* #param {Object} arg - Event handler args
* #param {string} arg.message - Message as string (NOT js OBJECT)
* #param {string} arg.origin - Message Origin
*/
const messageHandler = (arg) => {
// messaging can send only strings.
// If you need to pass complex JSON you need to stringify and parse objects
const message = JSON.parse(arg.message)
console.log("message:")
console.log(message)
if (message.type === "execution-canceled") {
// you have all props of message like message.reason
dialog.close();
return
}
if (message.type === "execution-confirmed") {
// execute than close popup
// if you need to close popup than execute, just run dialog.close()
// before doStuff() and remove finally method call
doStuff()
.then(() => console.log("doStuff .then"))
.catch(error => console.error(error))
.finally(() => dialog.close())
}
}
//////////////////////////////////////////
dialog.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Contoso Task Pane Add-in</title>
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
<!-- For the Office UI Fabric, go to http://aka.ms/office-ui-fabric to learn more. -->
<link rel="stylesheet" href="https://appsforoffice.microsoft.com/fabric/2.1.0/fabric.min.css">
<link rel="stylesheet" href="https://appsforoffice.microsoft.com/fabric/2.1.0/fabric.components.min.css">
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.1.min.js"></script>
<script defer="defer" src="dialog.js"></script>
</head>
<body>
<p class="ms-font-xxl ms-fontColor-neutralSecondary ms-fontWeight-semilight">Pick a number</p>
<button class="ms-Button ms-Button--primary" id="run">
<span class="ms-Button-icon"><i class="ms-Icon ms-Icon--plus"></i></span>
<span class="ms-Button-label" id="runbutton-text">RUN</span>
<span class="ms-Button-description" id="runbutton-desc">Run Func</span>
</button>
<button class="ms-Button ms-Button--primary" id="close">
<span class="ms-Button-icon"><i class="ms-Icon ms-Icon--plus"></i></span>
<span class="ms-Button-label" id="closebutton-text">EXIT</span>
<span class="ms-Button-description" id="closebutton-desc">Exit Func</span>
</button>
<form>
<label for="fname">First name:</label><br>
<input type="text" id="fname" name="fname"><br>
<label for="lname">Last name:</label><br>
<input type="text" id="lname" name="lname">
</form>
</body>
</html>
dialog.js
Office.initialize = () => {
document.getElementById("run").onclick = onExecutionConfirmed;
document.getElementById("close").onclick = onExcecutionCanceled;
}
const send = (message) => {
Office.context.ui.messageParent(JSON.stringify(message));
}
const onExecutionConfirmed = () => send({ type: 'execution-confirmed' })
const onExcecutionCanceled = () => send({
type: 'execution-canceled',
reason: "additional infos can be placed in any props of this serialized JSON object"
})
With a QR code vcard, the user scans the code with their phone and then the dialog with the "add to contacts" pops up on their phone, such as the code below:
How can I do the same but instead of a QR code scan, I want it to do the same with a button click.
I have tried the following:
var btn = document.getElementById(“clickMe”);
btn.addEventListener(“click”, loadvcard);
function loadvcard(){
url = "BEGIN%3AVCARD%0AVERSION%3A3.0%0AN%3ADoe%3BJohn%0AFN%3AJohn%20Doe%0ATITLE%3A08002221111%0AORG%3AStackflowover%0AEMAIL%3BTYPE%3DINTERNET%3Ajohndoe%40gmail.com%0AEND%3AVCARD";
window.open(url);
}
You can open your vcard in the browser as a data url if you want.
Your code would be:
var btn = document.getElementById(“clickMe”);
btn.addEventListener(“click”, loadvcard);
function loadvcard(){
var data = "BEGIN%3AVCARD%0AVERSION%3A3.0%0AN%3ADoe%3BJohn%0AFN%3AJohn%20Doe%0ATITLE%3A08002221111%0AORG%3AStackflowover%0AEMAIL%3BTYPE%3DINTERNET%3Ajohndoe%40gmail.com%0AEND%3AVCARD";
window.open("data:text/x-vcard;urlencoded," + data);
}
Try to use the web share api, it works.
<html>
<title>
Web Share API
</title>
<body>
<div>
<div>
<button onclick="shareVcard" id="shareFilesButton">Share Files</button>
</div>
</div>
</body>
<script>
document.getElementById('shareFilesButton').addEventListener("click", () => shareVcard())
function shareVcard() {
fetch("sample.vcf")
.then(function(response) {
return response.text()
})
.then(function(text) {
var file = new File([text], "sample.vcf", {type: 'text/vcard'});
var filesArray = [file];
var shareData = { files: filesArray };
if (navigator.canShare && navigator.canShare(shareData)) {
// Adding title afterwards as navigator.canShare just
// takes files as input
shareData.title = "vcard";
navigator.share(shareData)
.then(() => console.log('Share was successful.'))
.catch((error) => console.log('Sharing failed', error));
} else {
console.log("Your system doesn't support sharing files.");
}
});
}
</script>
</html>
I have a situation where I have two different sites, siteA.com and siteB.com, which need to share a common piece of information when a visitor navigates from siteA to siteB. I don't have access to the server-side code or navigation links from siteA, only limitied customizations and javascript. In order to share the information I have built a new page that is fully under my control at siteC.com, and then added this page as an iframe to both siteA and siteB. I am using the postMessage method to get and set the cookie from within the iframe which is working fine from each site, however I actually end up with two different cookies, one for each siteA and siteB even though the cookie belongs to siteC because it was set by the page in the iframe, confirmed through F12 debugger. I would have expected to have a single cookie and both sites could share the same cookie via the iframe, am I missing something here, should this be possible or is there another way to do this?
This is the code for my page at siteC that gets loaded into the iframe
<!DOCTYPE html>
<html>
<head>
<title>iframe source</title>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
$(function () {
var ck = document.cookie;
var expDate = new Date();
expDate.setFullYear(expDate.getFullYear() + 20)
$("#auditlog").append("iframe loaded<br/>");
if (ck) {
$("#auditlog").append("cookie exists<br/>");
} else {
$("#auditlog").append("cookie not set<br/>");
}
// Assign handler to message event
if (window.addEventListener) {
window.addEventListener('message', messageHandler, false);
} else if (window.attachEvent) { // ie8
window.attachEvent('onmessage', messageHandler);
}
})
function messageHandler(e) {
var msg = {};
var response;
// Check origin
if (e.origin === 'http://siteA' || e.origin === 'http://siteB') {
// Retrieve data sent in postMessage
msg = JSON.parse(e.data);
if (msg.action == "getCookie") {
response = getCookie();
} else if (msg.action == "setCookie") {
setCookie(msg.payload);
response = "cookie set";
} else {
response = "action not supported";
}
// Send reply to source of message
e.source.postMessage(response, e.origin);
}
}
function setCookie(cookieVal) {
var expDate = new Date();
expDate.setFullYear(expDate.getFullYear() + 20)
document.cookie = cookieVal + "; expires=" + expDate.toUTCString();
}
function getCookie() {
return document.cookie;
}
</script>
</head>
<body>
<div id="auditlog"></div>
<div id="cookieinfo"></div>
</body>
</html>
And this is code for my pages at siteA and siteB, both are using this same code, this is a sample I set up in order to test the set and get cookie functions in the iframe
<!DOCTYPE html>
<html>
<head>
<title>Main content page</title>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
$(function () {
// Assign handler to message event
if (window.addEventListener) {
window.addEventListener('message', messageHandler, false);
} else if (window.attachEvent) { // ie8
window.attachEvent('onmessage', messageHandler);
}
$("#btnGetIframeCookie").click(function () {
var postMsg = {
action:"getCookie"
}
// get reference to window inside the iframe
var wn = document.getElementById('cookieiframe').contentWindow;
// postMessage arguments: data to send, target origin
wn.postMessage(JSON.stringify(postMsg), 'http://siteC');
})
$("#btnSetIframeCookie").click(function () {
var cookieVal = $("#txtCookieValue").val();
var postMsg = {
action: "setCookie",
payload: cookieVal
}
var wn = document.getElementById('cookieiframe').contentWindow;
// postMessage arguments: data to send, target origin
wn.postMessage(JSON.stringify(postMsg), 'http://siteC');
})
})
function messageHandler(e) {
if (e.origin === 'http://siteC') {
$("#divMessages").append("response from iframe: <br/>" + e.data + "<br/>");
}
}
</script>
</head>
<body>
<div>
This is the iframe container
</div>
<div>
<input type="button" id="btnGetIframeCookie" value="Get iframe cookie" />
</div>
<div>
<input type="text" size="60" id="txtCookieValue" />
<input type="button" id="btnSetIframeCookie" value="Set iframe cookie" />
</div>
<iframe id="cookieiframe" src="http://siteC/iframe/index.html" style="width: 300px; height: 300px; border:1px solid black;"></iframe>
<div id="divMessages"></div>
</body>
</html>
Using this setup, if I set a cookie from siteA via the iframe with a value of "keyabc=value123" for example, I can then read that same cookie back, but when I go to siteB which has the same page in the iframe there, I don't have a cookie until I set one there, for example "keyabc=value456". Now if I look at my actual cookie files at C:\Users\aakoehle\AppData\Local\Packages\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\AC\#!001\MicrosoftEdge\Cookies I see two files, one with each of the values I set and both have the path of siteC. I also launched the F12 tools for each browser tab, each tab shows it's own cookie belonging to siteC.
-- UPDATE --
With the current version of my code posted here I am now only seeing the cookie issue in the Edge browser. Chrome and IE are sharing a single cookie between siteA and siteB as expected.
Here's an example for sharing data between cross origin sites, using localStorage and postMessage.
site1 : localhost:9091
<html>
<body>
<h1>site 1</h1>
<button id='postBtn'>Post message</button>
<br/>
<iframe id='commonSite' src='http://localhost:9093/commonSite.html' style='height:150px'></iframe>
<script>
(function () {
var commonSite = document.querySelector('#commonSite').contentWindow;
var postCounter = localStorage.getItem('postCounter');
postCounter = postCounter != null ? +postCounter : 1;
var commonOrigin = 'http://localhost:9093';
document.querySelector('#postBtn').onclick = function () {
commonSite.postMessage(postCounter++, commonOrigin);
localStorage.setItem('postCounter', postCounter);
console.log('site 1 posted');
}
})();
</script>
</body>
</html>
site2: localhost:9092
<html>
<body>
<h1>site 2</h1>
<button id='postBtn'>Post message</button>
<br/>
<iframe id='commonSite' src='http://localhost:9093/commonSite.html' style='height:150px'></iframe>
<script>
(function () {
var commonSite = document.querySelector('#commonSite').contentWindow;
var postCounter = localStorage.getItem('postCounter');
postCounter = postCounter != null ? +postCounter : 1;
var commonOrigin = 'http://localhost:9093';
document.querySelector('#postBtn').onclick = function () {
commonSite.postMessage(postCounter++, commonOrigin);
localStorage.setItem('postCounter', postCounter);
console.log('site 2 posted');
}
})();
</script>
</body>
</html>
commonSite: localhost:9093
<html>
<body>
<h3>Common site</h1>
<h4> Site 1 count: <span id='count1'></span></h3>
<h4> Site 2 count: <span id='count2'></span></h3>
<script>
(function () {
console.log('Adding message listener');
var origin1 = 'http://localhost:9091';
var origin2 = 'http://localhost:9092';
var count1 = document.querySelector('#count1');
var count2 = document.querySelector('#count2');
if(localStorage.getItem('count1')) {
count1.textContent = localStorage.getItem('count1');
}
if(localStorage.getItem('count2')) {
count2.textContent = localStorage.getItem('count2');
}
window.addEventListener('message', function (event) {
var origin = event.origin;
var data = event.data;
if(origin === origin1) {
localStorage.setItem('count1', data);
count1.textContent = localStorage.getItem('count1');
} else if(origin === origin2) {
localStorage.setItem('count2', data);
count2.textContent = localStorage.getItem('count2');
}
console.log('received (' + data + ') from ' + origin);
}, false);
})();
</script>
</body>
</html>