diff --git a/src/manifest.json b/src/manifest.json index 14949fa..5510fd2 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Panorama View", "description": "Tab Groups with Panorama View", - "version": "0.9.0", + "version": "0.9.1", "icons": { "16": "gfx/logo/logo-16.png", "24": "gfx/logo/logo-24.png", diff --git a/src/panorama/css/tab.css b/src/panorama/css/tab.css index e279385..9f2eadc 100644 --- a/src/panorama/css/tab.css +++ b/src/panorama/css/tab.css @@ -18,6 +18,19 @@ transition: opacity 400ms cubic-bezier(.08,.82,.17,1), transform 400ms cubic-bezier(.08,.82,.17,1); } +/* drag protection */ +.tab::after { + content: ''; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + z-index: 5; +} + .tab:hover, .tab.inactive:hover, .tab.selected { box-shadow: 0 1px 4px var(--color-shadow), 0 0 0 3px var(--color-tab-border); } @@ -41,45 +54,38 @@ } .tab .thumbnail { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - - background-size: cover; - background-position: top; + width: 100%; + height: 100%; + object-fit: cover; z-index: 0; } +.tab .thumbnail:not([src]) { + visibility: hidden; +} + .tab .favicon { width: 16px; height: 16px; + object-fit: cover; margin: 3px; padding: 2px; position: absolute; - left: 0; - top: 0; border-radius: 2px; - background-size: contain; - background-repeat: no-repeat; - background-origin: content-box; background-color: var(--color-tab-overlay); box-shadow: 0 0 0 1px var(--color-shadow); - - display: none; - z-index: 10; + z-index: 1; } -.tab .favicon.visible { - display: block; +.tab .favicon:not([src]) { + visibility: hidden; } .tab .close { @@ -181,24 +187,17 @@ mask-image: url(../gfx/plus.svg); } -.small .tab .favicon[style] + .thumbnail, .tiny .tab .favicon[style] + .thumbnail { - display: none; +.small .tab .favicon[src] + .thumbnail, .tiny .tab .favicon[src] + .thumbnail { + visibility: hidden; } .small .tab .favicon, .tiny .tab .favicon { - width: auto; - height: auto; + width: calc(100% - 4px); + height: calc(100% - 4px); margin: 0; padding: 2px; - - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - background-size: cover; background-color: transparent; box-shadow: none; } @@ -232,17 +231,11 @@ margin: 2px; padding: 0; - position: absolute; - left: 0; - top: 0; - border-radius: 0px; background-color: transparent; box-shadow: none; - - z-index: 10; } .list .tab .close { diff --git a/src/panorama/js/html.groups.js b/src/panorama/js/html.groups.js index de817c2..beda01a 100644 --- a/src/panorama/js/html.groups.js +++ b/src/panorama/js/html.groups.js @@ -92,8 +92,7 @@ export function create(group) { }, false); input.addEventListener('input', function(event) { - name.innerHTML = ''; - name.appendChild(document.createTextNode(this.value)) + name.textContent = this.value; input.style.width = name.getBoundingClientRect().width + 'px'; }, false); @@ -248,8 +247,7 @@ export function fitTabsInGroup(tabGroupNode) { let tabsNode = tabGroupNode.querySelector('.tabs'); let childNodes = tabsNode.childNodes; - tabGroupNode.querySelector('.tab_count').innerHTML = ''; - tabGroupNode.querySelector('.tab_count').appendChild(document.createTextNode(childNodes.length-1)); + tabGroupNode.querySelector('.tab_count').textContent = childNodes.length - 1; // fit let rect = tabsNode.getBoundingClientRect(); diff --git a/src/panorama/js/html.tabs.js b/src/panorama/js/html.tabs.js index 753603a..e27b438 100644 --- a/src/panorama/js/html.tabs.js +++ b/src/panorama/js/html.tabs.js @@ -8,8 +8,8 @@ import * as drag from './view.drag.js'; export function create(tab) { - let thumbnail = newElement('div', {class: 'thumbnail'}); - let favicon = newElement('div', {class: 'favicon'}); + let thumbnail = newElement('img', {class: 'thumbnail'}); + let favicon = newElement('img', {class: 'favicon'}); let close = newElement('div', {class: 'close'}); let title = newElement('span'); let nameContainer = newElement('div', {class: 'title'}, [title]); @@ -53,57 +53,52 @@ export function get(tabId) { } export async function update(tabNode, tab) { - if (tabNode) { - tabNode.querySelector('.title span').innerHTML = ''; - tabNode.querySelector('.title span').appendChild(document.createTextNode(tab.title)); + tabNode.querySelector('.title span').textContent = tab.title; - tabNode.title = tab.title + ((tab.url.substr(0, 5) !== 'data:') ? ' - ' + decodeURI(tab.url) : ''); + tabNode.title = tab.title + ((tab.url.substr(0, 5) != 'data:') ? ' - ' + decodeURI(tab.url) : ''); - if(tab.discarded) { + if (tab.discarded) { tabNode.classList.add('inactive'); - }else{ + } else { tabNode.classList.remove('inactive'); } } } -export async function updateThumbnail(tabNode, tabId, thumbnail) { - - const formatThumbnail = function(data) { - return (data) ? 'url(' + data + ')' : ''; +export async function updateFavicon(tabNode, tab) { + if (tabNode) { + if (tab.favIconUrl && + tab.favIconUrl.substr(0, 22) != 'chrome://mozapps/skin/' && + tab.favIconUrl != tab.url) { + tabNode.querySelector('.favicon').src = tab.favIconUrl; + } } +} + +export async function updateThumbnail(tabNode, tabId, thumbnail) { if (tabNode) { if (!thumbnail) thumbnail = await browser.sessions.getTabValue(tabId, 'thumbnail'); - tabNode.querySelector('.thumbnail').style.backgroundImage = formatThumbnail(thumbnail); + if (thumbnail) tabNode.querySelector('.thumbnail').src = thumbnail; } } export async function setActive() { - - var lastActiveTabId = -1; - var lastAccessed = 0; let tabs = await browser.tabs.query({currentWindow: true}); - for (let tab of tabs) { - if (tab.lastAccessed > lastAccessed && get(tab.id)) { - lastAccessed = tab.lastAccessed; - lastActiveTabId = tab.id; - } - } - - let tabNode = get(lastActiveTabId); - let activeNode = document.querySelector('.tab.active'); + tabs.sort((tabA, tabB) => { + return tabB.lastAccessed - tabA.lastAccessed; + }); + + const activeTabId = (tabs[0].url == browser.runtime.getURL('panorama/view.html')) ? tabs[1].id : tabs[0].id ; - if (activeNode) { - activeNode.classList.remove('active'); - } + const tabNode = get(activeTabId); + const activeNode = document.querySelector('.tab.active'); - if (tabNode) { - tabNode.classList.add('active'); - } + if (activeNode) activeNode.classList.remove('active'); + if (tabNode) tabNode.classList.add('active'); } export async function insert(tabNode, tab) { diff --git a/src/panorama/js/view.drag.js b/src/panorama/js/view.drag.js index 9657fe7..e1308fc 100644 --- a/src/panorama/js/view.drag.js +++ b/src/panorama/js/view.drag.js @@ -9,13 +9,7 @@ let selectedTabs = []; -async function moveTabs(e, windowId, tabGroupId, index) { - - let tabIds = []; - for (const tabIdData of e.dataTransfer.items) { - const tabId = Number(e.dataTransfer.getData(tabIdData.type)); - tabIds.push(tabId); - } +async function moveTabs(tabIds, windowId, tabGroupId, index) { browser.tabs.move(tabIds, {index: index, windowId: windowId}); @@ -25,6 +19,18 @@ async function moveTabs(e, windowId, tabGroupId, index) { } +function getTabIds(e) { + let tabIds = []; + for (const tabIdData of e.dataTransfer.items) { + if (tabIdData.type.includes('text/panorama-view-tab-id-')) { + const tabId = Number(e.dataTransfer.getData(tabIdData.type)); + tabIds.push(tabId); + } + } + return tabIds; +} + + // view events export function viewDragOver(e) { e.preventDefault(); // Necessary. Allows us to drop. @@ -40,10 +46,10 @@ export async function viewDrop(e) { // move the tab node let groupNode = html.groups.get(tabGroup.id); + + const tabIds = getTabIds(e); - for (const tabIdData of e.dataTransfer.items) { - const tabId = Number(e.dataTransfer.getData(tabIdData.type)); - + for (const tabId of tabIds) { const tabNode = html.tabs.get(tabId); if (tabNode) { groupNode.querySelector('.newtab').insertAdjacentElement('beforebegin', tabNode); @@ -55,7 +61,7 @@ export async function viewDrop(e) { } // ---- - moveTabs(e, currentWindowId, tabGroup.id, -1); + moveTabs(tabIds, currentWindowId, tabGroup.id, -1); return false; } @@ -78,8 +84,9 @@ export async function groupDrop(e) { } // move the tab node - for (const tabIdData of e.dataTransfer.items) { - const tabId = Number(e.dataTransfer.getData(tabIdData.type)); + const tabIds = getTabIds(e); + + for (const tabId of tabIds) { const tabNode = html.tabs.get(tabId); if (tabNode) { groupNode.querySelector('.newtab').insertAdjacentElement('beforebegin', tabNode); @@ -94,7 +101,7 @@ export async function groupDrop(e) { const tabGroupId = Number(groupNode.id.substr(8)); const currentWindowId = (await browser.windows.getCurrent()).id; - moveTabs(e, currentWindowId, tabGroupId, -1); + moveTabs(tabIds, currentWindowId, tabGroupId, -1); return false; } @@ -147,14 +154,19 @@ export function tabDragStart(e) { e.dataTransfer.effectAllowed = 'move'; + const tabId = Number(this.id.substr(3)); + if (!selectedTabs.includes(tabId)) { + clearTabSelection(e); + } + if (selectedTabs.length == 0) { - selectTab(Number(this.id.substr(3))); + selectTab(tabId); } for (const i in selectedTabs) { const tabNode = html.tabs.get(selectedTabs[i]); - e.dataTransfer.setData('text/panorama-view-tab-id-'+i, selectedTabs[i]); + e.dataTransfer.setData(`text/panorama-view-tab-id-${i}`, selectedTabs[i]); tabNode.classList.add('drag'); } @@ -178,6 +190,14 @@ export async function tabDrop(e) { tabNode = tabNode.parentNode; } // ---- + + const tabIds = getTabIds(e); + + // abort if you drop over moved tab + for (const tabId of tabIds) { + if (html.tabs.get(tabId) == tabNode) return false; + } + // ---- // get taget tab group ID let groupNode = e.target; @@ -204,9 +224,9 @@ export async function tabDrop(e) { // move the tab node - for (const tabIdData of e.dataTransfer.items) { - const tabId = Number(e.dataTransfer.getData(tabIdData.type)); + for (const tabId of tabIds) { const _tabNode = html.tabs.get(tabId); + if (_tabNode == tabNode) return false; // abort if you drop over moved tab if (_tabNode) { if (dropBefore) { tabNode.insertAdjacentElement('beforebegin', _tabNode); @@ -230,7 +250,7 @@ export async function tabDrop(e) { // find new index let toIndex = Number(tab.index); - const fromTabId = Number(e.dataTransfer.getData(e.dataTransfer.items[0].type)); + const fromTabId = tabIds[0]; let fromIndex = (await browser.tabs.get(fromTabId)).index; if (fromIndex < toIndex) { @@ -244,7 +264,7 @@ export async function tabDrop(e) { } // ---- - moveTabs(e, currentWindowId, tabGroupId, toIndex); + moveTabs(tabIds, currentWindowId, tabGroupId, toIndex); return false; } diff --git a/src/panorama/js/view.events.js b/src/panorama/js/view.events.js index 557a313..872d287 100644 --- a/src/panorama/js/view.events.js +++ b/src/panorama/js/view.events.js @@ -39,12 +39,12 @@ export async function tabCreated(tab) { tab.groupId = await addon.tabs.getGroupId(tab.id); } - let tabNode = html.tabs.create(tab); + const tabNode = html.tabs.create(tab); html.tabs.update(tabNode, tab); await html.tabs.insert(tabNode, tab); - core.updateFavicon(tab); + html.tabs.updateFavicon(tabNode, tab); let tabGroupNode = html.groups.get(tab.groupId); html.groups.fitTabs(tabGroupNode); @@ -60,14 +60,16 @@ export async function tabRemoved(tabId, removeInfo) { } export async function tabUpdated(tabId, changeInfo, tab) { + + const tabNode = html.tabs.get(tabId); if (core.viewWindowId == tab.windowId){ - html.tabs.update(html.tabs.get(tabId), tab); + html.tabs.update(tabNode, tab); } if (changeInfo.pinned != undefined) { if (changeInfo.pinned) { - html.tabs.get(tabId).remove(); + tabNode.remove(); html.groups.fitTabs(); html.tabs.setActive(); } else { @@ -79,10 +81,10 @@ export async function tabUpdated(tabId, changeInfo, tab) { } await tabCreated(tab); - core.updateFavicon(tab); + html.tabs.updateFavicon(tabNode, tab); } } else { - core.updateFavicon(tab); + html.tabs.updateFavicon(tabNode, tab); } } diff --git a/src/panorama/js/view.js b/src/panorama/js/view.js index 8ec42e5..9e46f11 100644 --- a/src/panorama/js/view.js +++ b/src/panorama/js/view.js @@ -52,11 +52,12 @@ async function initialize() { } }, false); - document.addEventListener('visibilitychange', () => { + document.addEventListener('visibilitychange', async() => { if (document.hidden) { browser.tabs.onUpdated.removeListener(captureThumbnail); viewLastAccessed = (new Date).getTime(); } else { + await captureThumbnails(); browser.tabs.onUpdated.addListener(captureThumbnail); } }, false); @@ -179,7 +180,7 @@ async function initializeTabNodes() { html.tabs.update(tabNode, tab); html.tabs.updateThumbnail(tabNode, tab.id); - updateFavicon(tab, tabNode); + html.tabs.updateFavicon(tabNode, tab); if (!fragments[tab.groupId]) { fragments[tab.groupId] = document.createDocumentFragment(); @@ -202,18 +203,9 @@ async function initializeTabNodes() { export async function captureThumbnail(tabId, changeInfo, tab) { - - let update = true; - - if (tab && tab.lastAccessed < viewLastAccessed) { - update = false; - } - - if (update) { - const thumbnail = await browser.tabs.captureTab(tabId, {format: 'jpeg', quality: 70, scale: 0.25}); - html.tabs.updateThumbnail(html.tabs.get(tabId), tabId, thumbnail); - browser.sessions.setTabValue(tabId, 'thumbnail', thumbnail); - } + const thumbnail = await browser.tabs.captureTab(tabId, {format: 'jpeg', quality: 70, scale: 0.25}); + html.tabs.updateThumbnail(html.tabs.get(tabId), tabId, thumbnail); + browser.sessions.setTabValue(tabId, 'thumbnail', thumbnail); } let viewLastAccessed = 0; @@ -223,55 +215,11 @@ async function captureThumbnails() { for(const tab of await tabs) { if (tab.lastAccessed > viewLastAccessed) { - captureThumbnail(tab.id); + await captureThumbnail(tab.id); } } } -async function testImage(url) { - return new Promise(function (resolve, reject) { - - let img = new Image(); - - img.onerror = img.onabort = function () { - reject('error'); - }; - - img.onload = function () { - resolve('success'); - }; - - img.src = url; - }); -} - -export async function updateFavicon(tab, tabNode) { - - tabNode = tabNode || html.tabs.get(tab.id); - - if (!tabNode) return; - - let faviconNode = tabNode.querySelector('.favicon'); - - if (faviconNode) { - if (tab.favIconUrl && - tab.favIconUrl.substr(0, 22) != 'chrome://mozapps/skin/' && - tab.favIconUrl != tab.url) { - testImage(tab.favIconUrl).then( - _ => { - faviconNode.style.backgroundImage = `url(${tab.favIconUrl})`; - faviconNode.classList.add('visible'); - }, _ => { - faviconNode.removeAttribute('style'); - faviconNode.classList.remove('visible'); - } - ); - } else { - faviconNode.removeAttribute('style'); - faviconNode.classList.remove('visible'); - } - } -} diff --git a/src/panorama/view.html b/src/panorama/view.html index ee78cd3..761185d 100644 --- a/src/panorama/view.html +++ b/src/panorama/view.html @@ -5,7 +5,7 @@ Panorama View - +