passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
199 lines (180 loc) • 7.17 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2021 Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) 2021 Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 3.3.0
*/
import {v4 as uuidv4} from "uuid";
import browser from "webextension-polyfill";
import DomUtils from "../Dom/DomUtils";
/**
* An InFormMenuField is represented by a DOM element identified as a menu field
*/
class InFormMenuField {
/**
* Default constructor
* @param field
*/
constructor(field) {
/** The field to which the in-form is attached */
this.field = field;
/** An unique identifier for the iframe */
this.iframeId = uuidv4();
/** Flag telling if the user is mousing over the menu (iframe) */
this.isMenuMousingOver = false;
/** The scrollable field parent */
this.scrollableFieldParent = null;
this.bindCallbacks();
this.insertInformMenuIframe();
this.handleRemoveEvent();
this.handleScrollEvent();
}
/**
* Binds methods callbacks
*/
bindCallbacks() {
this.removeInFormMenu = this.removeInFormMenu.bind(this);
this.removeMenuIframe = this.removeMenuIframe.bind(this);
this.destroy = this.destroy.bind(this);
}
/** MENU INSERTION */
/**
* Insert an in-form menu iframe
*/
async insertInformMenuIframe() {
const iframes = document.querySelectorAll('iframe');
// Use of Array prototype some method cause NodeList is not an array !
const iframeId = this.iframeId;
const isIframeAlreadyInserted = Array.prototype.some.call(iframes, iframe => iframe.id === iframeId);
if (!isIframeAlreadyInserted) {
const iframe = await this.createMenuIframe();
this.handleMenuClicked(iframe);
}
}
/**
* Create an iframe dedicated to the menu
* @return {HTMLIFrameElement} The created iframe
*/
async createMenuIframe() {
// IMPORTANT: Calculate position before inserting iframe in document to avoid issue
const {top, left} = this.calculateIframePosition();
const portId = await port.request("passbolt.port.generate-id", "InFormMenu");
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const browserExtensionUrl = browser.runtime.getURL("/");
iframe.id = this.iframeId;
iframe.style.position = "fixed";
iframe.style.top = `${top}px`;
iframe.style.left = `${left}px`;
iframe.style.border = 0;
iframe.style.width = '370px'; // width of the menu 350px + 20px to display shadows
iframe.style.height = '220px'; // For 3 items in a row to be display
iframe.style.zIndex = "123456";
iframe.contentWindow.location = `${browserExtensionUrl}webAccessibleResources/passbolt-iframe-in-form-menu.html?passbolt=${portId}`;
return iframe;
}
/**
* Calculates the position on the screen of the DOM field
* @return {{top: number, left: number}}
*/
calculateIframePosition() {
let x = 0;
let y = 0;
let currentElement = this.field;
let hasScroll = false;
const isInIframe = this.field.ownerDocument !== document;
const {top, left, height, width} = this.field.getBoundingClientRect();
// If the field is in iframe get the top and left of the iframe else get the top and the left of the body
const {top: topBody, left: leftBody} = isInIframe ? this.field.ownerDocument.defaultView.frameElement.getBoundingClientRect() : document.documentElement.getBoundingClientRect();
/*
* We loop to calculate the cumulated position of the field
* from its ancestors and itself differential offset / scroll position
*/
while (currentElement && !isNaN(currentElement.offsetLeft) && !isNaN(currentElement.offsetTop)) {
hasScroll = hasScroll || currentElement.scrollLeft + currentElement.scrollTop > 0;
x += currentElement.offsetLeft - currentElement.scrollLeft;
y += currentElement.offsetTop - currentElement.scrollTop;
currentElement = currentElement.offsetParent;
}
/*
* If there is no scroll on the page, we use the getBoundingClientRect to have the position of the field
* this avoid a position issue if there is a transform style in css
*/
if (!hasScroll && !isInIframe) {
x = left + width - 367; // (-370 width of the iframe + 10 to adjust with the shadow) (-7 adjustment of the call to action menu (18-25))
y = top + height; // Calculate the bottom position of the input
} else {
// Then we add the body offset (notably in case of window scroll) + some local adjustments (margin / vertical aligment ) to align with the call to action icon
x = x + leftBody + width - 367; // (-370 width of the iframe + 10 to adjust with the shadow) (-7 adjustment of the call to action menu (18-25))
y = y + topBody + height; // Calculate the bottom position of the input
}
return {top: y, left: x};
}
/**
* Whenever the user clicked on the call-to-action iframe
* @param iframe The menu iframe
*/
handleMenuClicked(iframe) {
/*
* We need to know which iframe the user click on. We cannot add a listener on iframe
* since there are from different domains (target page vs extension pagemods)
*/
iframe.addEventListener('mouseover', () => this.isMenuMousingOver = true);
iframe.addEventListener('mouseout', () => this.isMenuMousingOver = false);
}
/** MENU REMOVE */
/**
* Whenever the menu must be removed
*/
handleRemoveEvent() {
this.field.addEventListener("blur", this.removeInFormMenu);
}
/**
* Removes the in-form menu iframe from the field
*/
removeInFormMenu() {
const isIframeMouseOver = this.isMenuMousingOver;
const isActiveElement = document.activeElement === this.field;
if (!isIframeMouseOver && !isActiveElement) {
this.removeMenuIframe();
}
}
/**
* Remove the menu (iframe)
*/
removeMenuIframe() {
const iframes = document.querySelectorAll('iframe');
iframes.forEach(iframe => {
const identifierToMatch = this.iframeId;
if (iframe.id === identifierToMatch) {
iframe.parentNode.removeChild(iframe);
}
});
}
/** SCROLL REPOSITION */
/**
* Whenever the user scrolls the page
*/
handleScrollEvent() {
// Remove the in form menu
this.scrollableFieldParent = DomUtils.getScrollParent(this.field);
this.scrollableFieldParent.addEventListener('scroll', this.removeMenuIframe);
}
/** DESTROY */
/**
* Remove all listener and iframe to clean the page and avoid issue on extension update
*/
destroy() {
this.field.removeEventListener("blur", this.removeInFormMenu);
this.scrollableFieldParent.removeEventListener('scroll', this.removeMenuIframe);
this.removeMenuIframe();
}
}
export default InFormMenuField;