diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a76c68b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/0.0.1.zip
\ No newline at end of file
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
new file mode 100644
index 0000000..42b38f7
--- /dev/null
+++ b/_locales/en/messages.json
@@ -0,0 +1,23 @@
+{
+ "openInPopupWindow": {
+ "message": "Open in popup window"
+ },
+ "extensionDescription": {
+ "message": "Adds context menu entry to open link in popup window"
+ },
+ "popupHeight": {
+ "message": "Popup height (px)"
+ },
+ "closeWhenFocusedInitialWindow": {
+ "message": "Close popup when origin window is focused"
+ },
+ "popupWidth": {
+ "message": "Popup width (px)"
+ },
+ "tryOpenAtMousePosition": {
+ "message": "Try to open at mouse position (disabled — open at screen center)"
+ },
+ "hideBrowserControls": {
+ "message": "Hide browser controls (addressbar, tabbar, etc)"
+ }
+}
\ No newline at end of file
diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json
new file mode 100644
index 0000000..6995f38
--- /dev/null
+++ b/_locales/ru/messages.json
@@ -0,0 +1,23 @@
+{
+ "extensionDescription": {
+ "message": "Добавляет опцию в контекстное меню для ссылки для открытия её во вплывающем окне"
+ },
+ "closeWhenFocusedInitialWindow": {
+ "message": "Закрыть всплывающее окно, когда изначальное окно получает фокус"
+ },
+ "openInPopupWindow": {
+ "message": "Открыть во всплывающем окне"
+ },
+ "popupHeight": {
+ "message": "Высота окна (px)"
+ },
+ "popupWidth": {
+ "message": "Ширина окна (px)"
+ },
+ "tryOpenAtMousePosition": {
+ "message": "Пытаться открыть под курсором мыши (отключено — октрывать в центре экрана)"
+ },
+ "hideBrowserControls": {
+ "message": "Прятать элементы управления браузера (панель адреса, вкладок, и тд)"
+ }
+}
\ No newline at end of file
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..41c8e4a
--- /dev/null
+++ b/background.js
@@ -0,0 +1,93 @@
+let lastClientX, lastClientY, clientHeight, clientWidth, originWindowId;
+
+chrome.runtime.onMessage.addListener(
+ function (request, sender, sendResponse) {
+ lastClientX = request.lastClientX;
+ lastClientY = request.lastClientY;
+ clientHeight = request.clientHeight;
+ clientWidth = request.clientWidth;
+ }
+);
+
+const contextMenuItem = {
+ "id": "openInPopupWindow",
+ "title": chrome.i18n.getMessage('openInPopupWindow'),
+ "contexts": ["link"]
+};
+
+chrome.contextMenus.create(contextMenuItem);
+
+chrome.contextMenus.onClicked.addListener(function(clickData) {
+ /// load configs
+ loadUserConfigs(function(){
+ let originalWindowIsFullscreen = false;
+
+ /// store current windowId
+ chrome.windows.getCurrent(
+ function(originWindow){
+ if (originWindow.type !== 'popup') originWindowId = originWindow.id;
+
+ /// if original window is fullscreen, unmaximize it (for MacOS)
+ if (originWindow.state == 'fullscreen') {
+ originalWindowIsFullscreen = true;
+ chrome.windows.update(originWindow.id, {
+ 'state': 'maximized'
+ });
+ }
+ });
+
+ let dx, dy, height, width;
+
+ // height = window.screen.height * 0.65, width = window.screen.height * 0.5;
+ height = configs.popupHeight ?? 800, width = configs.popupWidth ?? 600;
+ height = parseInt(height); width = parseInt(width);
+
+ if (configs.tryOpenAtMousePosition == true && (lastClientX && lastClientY)) {
+ /// open at last known mouse position
+ dx = lastClientX - (width / 2), dy = lastClientY - (height / 2);
+ } else {
+ /// open at center of screen
+ dx = (window.screen.width / 2) - (width / 2), dy = (window.screen.height / 2) - (height / 2);
+ }
+ // }
+
+
+ /// check for screen overflow
+ if (dx < 0) dx = 0;
+ if (dy < 0) dy = 0;
+ if (dx + width > window.screen.width) dx = dx - (dx + width - window.screen.width);
+ if (dy + height > window.screen.height) dy = dy - (dy + height - window.screen.height);
+ dx = Math.round(dx); dy = Math.round(dy);
+
+ /// create popup window
+ setTimeout(function () {
+ chrome.windows.create({
+ 'url': clickData.linkUrl, 'type': configs.hideBrowserControls ? 'popup' : 'normal', 'width': width, 'height': height,
+ 'top': dy, 'left': dx
+ }, function (popupWindow) {
+ /// set coordinates again (workaround for old firefox bug)
+ chrome.windows.update(popupWindow.id, {
+ 'top': dy, 'left': dx
+ });
+
+ if (configs.closeWhenFocusedInitialWindow == false) return;
+
+ /// close popup on click parent window
+ function windowFocusListener(windowId) {
+ if (windowId == originWindowId) {
+ chrome.windows.onFocusChanged.removeListener(windowFocusListener);
+ chrome.windows.remove(popupWindow.id);
+
+ if (originalWindowIsFullscreen) chrome.windows.update(parentWindow.id, {
+ 'state': 'fullscreen'
+ });
+ }
+ }
+
+ setTimeout(function () {
+ chrome.windows.onFocusChanged.addListener(windowFocusListener);
+ }, 300);
+ });
+ }, originalWindowIsFullscreen ? 600 : 0)
+ });
+ });
\ No newline at end of file
diff --git a/configs.js b/configs.js
new file mode 100644
index 0000000..4529bb7
--- /dev/null
+++ b/configs.js
@@ -0,0 +1,29 @@
+const configs = {
+ 'closeWhenFocusedInitialWindow': true,
+ 'tryOpenAtMousePosition': true,
+ 'hideBrowserControls': true,
+ 'popupHeight': 800,
+ 'popupWidth': 600,
+}
+
+function loadUserConfigs(callback) {
+ const keys = Object.keys(configs);
+ chrome.storage.sync.get(
+ keys, function (userConfigs) {
+ const l = keys.length;
+ for (let i = 0; i < l; i++) {
+ let key = keys[i];
+
+ if (userConfigs[key] !== null && userConfigs[key] !== undefined)
+ configs[key] = userConfigs[key];
+
+ }
+
+ if (callback) callback(userConfigs);
+ }
+ );
+}
+
+function saveAllSettings() {
+ chrome.storage.sync.set(configs);
+}
\ No newline at end of file
diff --git a/content.js b/content.js
new file mode 100644
index 0000000..550f713
--- /dev/null
+++ b/content.js
@@ -0,0 +1,4 @@
+document.addEventListener("contextmenu", function (e) {
+ const el = document.elementFromPoint(e.clientX, e.clientY);
+ chrome.runtime.sendMessage({ lastClientX: e.clientX, lastClientY: e.clientY, clientHeight: el.clientHeight, clientWidth: el.clientWidth});
+});
\ No newline at end of file
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..b24406b
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..c801757
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,42 @@
+{
+ "manifest_version": 2,
+ "name": "Open in Popup window",
+ "description": "__MSG_extensionDescription__",
+ "default_locale": "en",
+ "version": "0.0.1",
+ "icons": {
+ "48": "icon.svg",
+ "96": "icon.svg",
+ "128": "icon.svg"
+ },
+ "background": {
+ "scripts": [
+ "background.js",
+ "configs.js"
+ ],
+ "persistent": false
+ },
+ "content_scripts": [
+ {
+ "matches": [
+ ""
+ ],
+ "js": [
+ "content.js"
+ ],
+ "run_at": "document_start"
+ }
+ ],
+ "permissions": [
+ "contextMenus",
+ "storage"
+ ],
+ "options_ui": {
+ "page": "options/options.html"
+ },
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "open_in_popup_window@emvaized.dev"
+ }
+ }
+}
\ No newline at end of file
diff --git a/options/options.css b/options/options.css
new file mode 100644
index 0000000..9f8889f
--- /dev/null
+++ b/options/options.css
@@ -0,0 +1,10 @@
+.option {
+ padding: 5px;
+ cursor: pointer;
+}
+
+.option:hover {
+ background-color: lightgrey;
+ transition: background-color 50ms ease-in-out;
+ border-radius: 4px;
+}
\ No newline at end of file
diff --git a/options/options.html b/options/options.html
new file mode 100644
index 0000000..ccfa216
--- /dev/null
+++ b/options/options.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/options/options.js b/options/options.js
new file mode 100644
index 0000000..d61747d
--- /dev/null
+++ b/options/options.js
@@ -0,0 +1,42 @@
+document.addEventListener("DOMContentLoaded", init);
+
+function init(){
+ loadUserConfigs(function(userConfigs){
+
+ const keys = Object.keys(configs);
+
+ for (let i = 0, l = keys.length; i < l; i++) {
+ const key = keys[i];
+
+ /// set corresponing input value
+ let input = document.getElementById(key.toString());
+
+ /// Set input value
+ if (input !== null && input !== undefined) {
+ if (input.type == 'checkbox') {
+ if ((userConfigs[key] !== null && userConfigs[key] == true) || (userConfigs[key] == null && configs[key] == true))
+ input.setAttribute('checked', 0);
+ else input.removeAttribute('checked', 0);
+ } else {
+ input.setAttribute('value', userConfigs[key] ?? configs[key]);
+ }
+
+ /// Set translated label for input
+ if (!input.parentNode.innerHTML.includes(chrome.i18n.getMessage(key))) {
+ input.parentNode.innerHTML += ' ' + chrome.i18n.getMessage(key);
+ }
+
+ input = document.querySelector('#' + key.toString());
+
+ /// Set event listener
+ input.addEventListener("input", function (e) {
+ let id = input.getAttribute('id');
+ let inputValue = input.getAttribute('type') == 'checkbox' ? input.checked : input.value;
+ configs[id] = inputValue;
+
+ saveAllSettings();
+ });
+ }
+ }
+ });
+}
\ No newline at end of file