/*
* Advanced Panels (a mod for Vivaldi)
* Written by LonM
* No Copyright Reserved
* ko translation by @dencion
* it by @folgore101
* de by @knoelli
* nl by @Vistaus
* jp by @nkay1005
* pt-br by @oinconquistado
* pl-pl by @supra107
*/
(function advancedPanels(){
"use strict";
const LANGUAGE = 'en_gb'; // en_gb or ko
const l10n = {
en_gb: {
title: 'Sessions',
new_session: 'New Session',
session_name_placeholder: 'Session Name',
all_windows: 'All Windows',
only_selected: 'Only Selected Tabs',
add_session_btn: 'Add Session',
sort_title: 'Sort by...',
sort_date: 'Sort by Date',
sort_name: 'Sort by Name',
sort_asc: 'Sort Ascending',
sort_desc: 'Sort Descending',
delete_button: 'Delete this session',
delete_prompt: 'Are you sure you want to delete $T?',
delete_number_sessions: 'Are you sure you want to delete $N selected sessions?',
delete_confirm: '⚠ Yes, Delete',
delete_abscond: 'No, don\'t.',
time_created_label: 'Created ',
open_new_window: 'Open in new window',
open_current: 'Open in current window'
},
ko: {
title: '세션',
new_session: '새로운 세션',
session_name_placeholder: '세션 이름',
all_windows: '모든 창',
only_selected: '선택한 탭만',
add_session_btn: '세션 추가',
sort_title: '정렬',
sort_date: '날짜순 정렬',
sort_name: '이름순 정렬',
sort_asc: '오름차순 정렬',
sort_desc: '내림차순 정렬',
delete_button: '이 세션을 지우기',
delete_prompt: '$T 세션을 지우시겠습니까?',
delete_number_sessions: '선택한 $N개의 세션을 지우시겠습니까?',
delete_confirm: '⚠네',
delete_abscond: '아니오',
time_created_label: '만든 시각 ',
open_new_window: '새 창에서 열기',
open_current: '현재 창에서 열기'
},
it: {
title: 'Sessioni',
new_session: 'Nuova sessione',
session_name_placeholder: 'Nome sessione',
all_windows: 'Tutte le finestre',
only_selected: 'Solo schede selezionate',
add_session_btn: 'Aggiungi sessione',
sort_title: 'Ordina per...',
sort_date: 'Ordina per data',
sort_name: 'Ordina per nome',
sort_asc: 'Ordine crescente',
sort_desc: 'Ordine decrescente',
delete_button: 'Elimina questa sessione',
delete_prompt: 'Sei sicuro di voler eliminare $T?',
delete_number_sessions: 'Sei sicuro di voler eliminare $N sessioni selezionate?',
delete_confirm: '⚠ Sì, Elimina',
delete_abscond: 'No, non farlo.',
time_created_label: 'Creata ',
open_new_window: 'Apri in una nuova finestra',
open_current: 'Apri nella finestra corrente'
},
de: {
title: 'Sitzungen',
new_session: 'Neue Sitzung',
session_name_placeholder: 'Name der Sitzung',
all_windows: 'Alle Fenster',
only_selected: 'Nur ausgewählte Tabs',
add_session_btn: 'Sitzung hinzufügen',
sort_title: 'Sortieren nach...',
sort_date: 'Sortieren nach Datum',
sort_name: 'Sortieren nach Namen',
sort_asc: 'Aufsteigend sortieren',
sort_desc: 'Absteigend sortieren',
delete_button: 'Diese Sitzung löschen',
delete_prompt: 'Wollen Sie $T wirklich löschen?',
delete_number_sessions: 'Wollen Sie die $N ausgewählten Sitzungen wirklich löschen?',
delete_confirm: '⚠ Ja, löschen',
delete_abscond: 'Nein',
time_created_label: 'Erstellt ',
open_new_window: 'In neuem Fenster öffnen',
open_current: 'Im aktuellen Fenster öffnen'
},
nl: {
title: 'Sessies',
new_session: 'Nieuwe sessie',
session_name_placeholder: 'Sessienaam',
all_windows: 'Alle vensters',
only_selected: 'Alleen geselecteerde tabbladen',
add_session_btn: 'Sessie toevoegen',
sort_title: 'Sorteren op...',
sort_date: 'Sorteren op datum',
sort_name: 'Sorteren op naam',
sort_asc: 'Oplopend sorteren',
sort_desc: 'Aflopend sorteren',
delete_button: 'Sessie verwijderen',
delete_prompt: 'Weet u zeker dat u $T wilt verwijderen?',
delete_number_sessions: 'Weet u zeker dat u $N geselecteerde sessies wilt verwijderen?',
delete_confirm: '⚠ Ja, verwijderen',
delete_abscond: 'Nee, behouden',
time_created_label: 'Toegevoegd om ',
open_new_window: 'Openen in nieuw venster',
open_current: 'Openen in huidig venster'
},
ja: {
title: 'セッション',
new_session: '新しいセッション',
session_name_placeholder: 'セッション名',
all_windows: '全てのウィンドウ',
only_selected: '選択したタブのみ',
add_session_btn: 'セッションを保存',
sort_title: '並べ替え...',
sort_date: '日付で並べ替え',
sort_name: 'セッション名で並べ替え',
sort_asc: '昇順に並べ替え',
sort_desc: '降順に並べ替え',
delete_button: 'セッションを削除',
delete_prompt: '$T を削除しますか?',
delete_number_sessions: '選択した$N個のセッションを削除しますか?',
delete_confirm: '⚠ 削除',
delete_abscond: 'キャンセル',
time_created_label: '作成日 ',
open_new_window: '新しいウィンドウで開く',
open_current: '現在のウィンドウで開く'
},
pt_br: {
title: 'Sessões',
new_session: 'Nova sessão',
session_name_placeholder: 'Nome da sessão',
all_windows: 'Todas as janelas',
only_selected: 'Apenas abas selecionadas',
add_session_btn: 'Adicionar sessão',
sort_title: 'Organizar por...',
sort_date: 'Organizar por data',
sort_name: 'Organizar por nome',
sort_asc: 'Organizar por ordem crescente',
sort_desc: 'Organizar por ordem decrescente',
delete_button: 'Deletar essa sessão',
delete_prompt: 'Você deseja deletar $T?',
delete_number_sessions: 'Você deseja deletar $N sessões selecionadas?',
delete_confirm: '⚠ Sim, delete',
delete_abscond: 'Não, não deletar.',
time_created_label: 'Criado em ',
open_new_window: 'Abrir em uma nova janela',
open_current: 'Abrir na janela atual'
},
pl_pl: {
title: 'Sesje',
new_session: 'Nowa sesja',
session_name_placeholder: 'Nazwa sesji',
all_windows: 'Wszystkie okna',
only_selected: 'Tylko zaznaczone karty',
add_session_btn: 'Dodaj sesję',
sort_title: 'Sortuj według...',
sort_date: 'Sortuj według daty',
sort_name: 'Sortuj według nazwy',
sort_asc: 'Sortuj rosnąco',
sort_desc: 'Sortuj malejąco',
delete_button: 'Usuń tę sesję',
delete_prompt: 'Czy jesteś pewien że chcesz usunąć $T?',
delete_number_sessions: 'Czy jesteś pewien że chcesz usunąć $N zaznaczonych sesji?',
delete_confirm: '⚠ Tak, usuń',
delete_abscond: 'Nie, nie usuwaj.',
time_created_label: 'Utworzono ',
open_new_window: 'Otwórz w nowym oknie',
open_current: 'Otwórz w obecnym oknie'
},
es_es: {
title: 'Sesiones',
new_session: 'Nueva sesión',
session_name_placeholder: 'Nombre',
all_windows: 'Todas las ventanas',
only_selected: 'Solo pestañas seleccionadas',
add_session_btn: 'Crrear Sesión',
sort_title: 'Ordenar por...',
sort_date: 'Ordenar por fecha',
sort_name: 'Ordenar por nombre',
sort_asc: 'Orden ascendente',
sort_desc: 'Orden inverso',
delete_button: 'Borrar esta sesión',
delete_prompt: '¿Seguro que quiere borrar la sesión $T?',
delete_number_sessions: '¿Seguro que quiere borrar las $N sesiones seleccionadas?',
delete_confirm: '⚠ Sí, eliminar',
delete_abscond: 'Hmm, mejor no.',
time_created_label: 'Creada el ',
open_new_window: 'Abrir en una nueva ventana',
open_current: 'Abrir en la ventana actual'
}
}[LANGUAGE];
/**
* Key is the ID of your advanced panel. This must be UNIQUE (across the whole vivaldi UI). If in doubt, append your name to ensure it is unique
* You can use this ID as a #selector in the advancedPanels.css file
* title: String, self explanatory
* url: String, a UNIQUE (amongst web panels) vivaldi:// url that points to a non-existent page. You must add this as a web panel
* switch: String of HTML, this will be set as the html in the panel switch button. E.g. an SVG
* initialHTML: String of HTML, this will be used to fill in the panel with a skeleton of HTML to use
* module: () => {onInit, onActivate} All necessary javascript should be included here.
* onInit: () => void. This will be called AFTER the advanced panel DOM is added, but BEFORE onActivate is called.
* onActivate: () => void. This will be called each time the advanced panel is opened AND IMMEDIATELY AFTER onInit.
*/
const CUSTOM_PANELS = {
sessions_lonm: {
title: l10n.title,
url: "vivaldi://sessions",
switch: ``,
initialHTML: `
${l10n.new_session}
${l10n.delete_prompt}
${l10n.time_created_label}
`,
module: function(){
/**
* Get selected session names
* @returns string array of names
*/
function getSelectedSessionNames(){
const selections = Array.from(document.querySelectorAll("#sessions_lonm li.selected"));
return selections.map(x => x.getAttribute("data-session-name"));
}
/**
* Open a session after its corresponding list item is clicked
* @param e click event
* REMARK: Hide the confirm box if it is open
* REMARK: If click was on a button, just ignore it
*/
function listItemClick(e){
if(isButton(e.target)){
return;
}
if(e.ctrlKey){
e.currentTarget.classList.toggle("selected");
} else {
const oldselect = document.querySelectorAll("#sessions_lonm li.selected");
oldselect.forEach(item => item.classList.remove("selected"));
e.currentTarget.classList.add("selected");
}
document.querySelector("#sessions_lonm .confirm").classList.remove("show");
}
/**
* Check if the target of a click is a button
* @param target an event target
*/
function isButton(target){
const tag = target.tagName.toLowerCase();
return (tag==="button" && target.className==="delete") || (tag==="svg" && target.parentElement.className==="delete");
}
/**
* Turns a date into a string that can be used in a file name
* Locale string seems to be the best at getting the correct time for any given timezone
* @param {Date} date object
*/
function dateToFileSafeString(date){
const badChars = /[\\/:\*\?"<>\|]/gi;
return date.toLocaleString().replace(badChars, '.');
}
/**
* Add a new session
* @param e button click event
*/
function newSessionClick(e){
let name = document.querySelector('#sessions_lonm .newSession input.session-name').value;
const windows = document.querySelector('#sessions_lonm .newSession input.all-windows').checked;
const selectedTabs = document.querySelector('#sessions_lonm .newSession input.selected-tabs').checked;
const markedTabs = document.querySelectorAll(".tab.marked");
if(name===""){
name = dateToFileSafeString(new Date());
}
vivaldi.windowPrivate.getCurrentId(window => {
const options = {
saveOnlyWindowId: windows ? 0 : window
};
if(selectedTabs && markedTabs && markedTabs.length>0){
options.ids = Array.from(markedTabs).map(tab => Number(tab.id.replace("tab-", "")));
}
vivaldi.sessionsPrivate.saveOpenTabs(name, options, ()=>{
document.querySelector('#sessions_lonm .newSession input.session-name').value = "";
document.querySelector('#sessions_lonm .newSession input.all-windows').checked = false;
document.querySelector('#sessions_lonm .newSession input.selected-tabs').checked = false;
updateList();
});
});
}
/**
* Change sort Order
* @param e click event
*/
function sortOrderChange(e){
document.querySelectorAll("#sessions_lonm .sortselector-button").forEach(el => {
el.classList.toggle("selected");
});
updateList();
}
/**
* Change sort Method
* @param e click event
*/
function sortMethodChange(e){
updateList();
}
/**
* User clicked remove button
* @param e click event
*/
function deleteClick(e){
const selectedSessions = getSelectedSessionNames();
if(selectedSessions.length === 1){
const delete_t = l10n.delete_prompt.replace('$T', selectedSessions[0]);
confirmMsg(delete_t);
} else {
const delete_n = l10n.delete_number_sessions.replace('$N', selectedSessions.length);
confirmMsg(delete_n);
}
}
/**
* Show the delete confirmation box with specified text
* @param msg string to use
*/
function confirmMsg(msg){
document.querySelector("#sessions_lonm .confirm p").innerText = msg;
document.querySelector("#sessions_lonm .modal-container").classList.add("show");
}
/**
* User confirmed remove
* @param e event
* REMARK: Want to remove all and only update UI after final removal
*/
function deleteConfirmClick(e){
const selections = getSelectedSessionNames();
for (let i = 0; i < selections.length-1; i++) {
vivaldi.sessionsPrivate.delete(selections[i],() => {});
}
vivaldi.sessionsPrivate.delete(selections[selections.length-1], ()=>{
updateList();
});
}
/**
* User cancelled remove
* @param e event
*/
function deleteCancelClick(e){
document.querySelector("#sessions_lonm .modal-container").classList.remove("show");
}
/**
* User clicked open (current) button
* @param e click event
*/
function openInCurrentWindowClick(e){
const selections = getSelectedSessionNames();
selections.forEach(item => {
vivaldi.sessionsPrivate.open(
vivaldiWindowId,
item,
{openInNewWindow: false}
);
});
}
/**
* User clicked open (new) button
* @param e click event
*/
function oneInNewWindowClick(e){
const selections = getSelectedSessionNames();
selections.forEach(item => {
vivaldi.sessionsPrivate.open(
vivaldiWindowId,
item,
{openInNewWindow: true}
);
});
}
/**
* Turn a date into a string to show underneath each session
* @param {Date} date
*/
function dateToString(date){
return date.toLocaleString();
}
/**
* Generate a list item for a session object
* @returns DOM list item
*/
function createListItem(session){
const template = document.querySelector("#sessions_lonm template.session_item");
const el = document.importNode(template.content, true);
el.querySelector("h3").innerText = session.name;
el.querySelector("h3").setAttribute("title", session.name);
const date = new Date(session.createDateJS);
el.querySelector("time").innerText = dateToString(date);
el.querySelector("time").setAttribute("datetime", date.toISOString());
el.querySelector("li").addEventListener("click", listItemClick);
el.querySelector(".open_new").addEventListener("click", oneInNewWindowClick);
el.querySelector(".open_current").addEventListener("click", openInCurrentWindowClick);
el.querySelector(".delete").addEventListener("click", deleteClick);
el.querySelector("li").setAttribute("data-session-name", session.name);
return el;
}
/**
* Sort the array of sessions
* @param sessions array of session objects - unsorted
* @returns sessions array of session objects - sorted
*/
function sortSessions(sessions){
const sortRule = document.querySelector("#sessions_lonm .sortselector-dropdown").value;
const sortDescending = document.querySelector("#sessions_lonm .direction-descending.selected");
if(sortRule==="visitTime" && sortDescending){
sessions.sort((a,b) => {return a.createDateJS - b.createDateJS;});
} else if (sortRule==="visitTime" && !sortDescending) {
sessions.sort((a,b) => {return b.createDateJS - a.createDateJS;});
} else if (sortRule==="title" && sortDescending) {
sessions.sort((a,b) => {return a.name.localeCompare(b.name);});
} else if (sortRule==="title" && !sortDescending) {
sessions.sort((a,b) => {return b.name.localeCompare(a.name);});
}
return sessions;
}
/**
* Create the dom list for the sessions
* @param sessions The array of session objects (already sorted)
* @returns DOM list of session items
*/
function createList(sessions){
const newList = document.createElement("ul");
sessions.forEach((session, index) => {
const li = createListItem(session, index);
newList.appendChild(li);
});
return newList;
}
/**
* Get the array of sessions and recreate the list in the panel
*/
function updateList(){
document.querySelector("#sessions_lonm .modal-container").classList.remove("show");
const existingList = document.querySelector("#sessions_lonm .sessionslist ul");
if(existingList){
existingList.parentElement.removeChild(existingList);
}
vivaldi.sessionsPrivate.getAll(items => {
const sorted = sortSessions(items);
const newList = createList(sorted);
document.querySelector("#sessions_lonm .sessionslist").appendChild(newList);
});
}
/**
* Update the session listing on activation of panel
*/
function onActivate(){
updateList();
}
/**
* Add the sort listeners on creation of panel
*/
function onInit(){
document.querySelectorAll("#sessions_lonm .sortselector-button").forEach(el => {
el.addEventListener("click", sortOrderChange);
});
document.querySelector("#sessions_lonm .sortselector-dropdown").addEventListener("change", sortMethodChange);
document.querySelector("#sessions_lonm .confirm .yes").addEventListener("click", deleteConfirmClick);
document.querySelector("#sessions_lonm .confirm .no").addEventListener("click", deleteCancelClick);
document.querySelector("#sessions_lonm .newSession .add-session").addEventListener("click", newSessionClick);
}
return {
onInit: onInit,
onActivate: onActivate
};
}
}
};
/**
* Observe for changes to the UI, e.g. if panels are hidden by going in to fullscreen mode
* This may require the panel buttons and panels to be re-converted
* Also, observe panels container, if class changes to switcher, the webstack is removed
*/
const UI_STATE_OBSERVER = new MutationObserver(records => {
convertWebPanelButtonstoAdvancedPanelButtons();
listenForNewPanelsAndConvertIfNecessary();
});
/**
* Observe UI state changes
*/
function observeUIState(){
const classInit = {
attributes: true,
attributeFilter: ["class"]
};
UI_STATE_OBSERVER.observe(document.querySelector("#browser"), classInit);
UI_STATE_OBSERVER.observe(document.querySelector("#panels-container"), classInit);
}
const PANEL_STACK_CREATION_OBSERVER = new MutationObserver((records, observer) => {
observer.disconnect();
listenForNewPanelsAndConvertIfNecessary();
});
/**
* Start listening for new web panel stack children and convert any already open ones
*/
function listenForNewPanelsAndConvertIfNecessary(){
const panelStack = document.querySelector("#panels .webpanel-stack");
if(panelStack){
WEBPANEL_CREATE_OBSERVER.observe(panelStack, {childList: true});
const currentlyOpen = document.querySelectorAll(".webpanel-stack .panel");
currentlyOpen.forEach(convertWebPanelToAdvancedPanel);
} else {
const panels = document.querySelector("#panels");
PANEL_STACK_CREATION_OBSERVER.observe(panels, {childList: true});
}
}
/**
* Observer that should check for child additions to web panel stack
* When a new child is added (a web panel initialised), convert it appropriately
*/
const WEBPANEL_CREATE_OBSERVER = new MutationObserver(records => {
records.forEach(record => {
record.addedNodes.forEach(convertWebPanelToAdvancedPanel);
});
});
/**
* Webview loaded a page. This means the src has been initially set.
* @param e load event
*/
function webviewLoaded(e){
e.currentTarget.removeEventListener("contentload", webviewLoaded);
convertWebPanelToAdvancedPanel(e.currentTarget.parentElement.parentElement);
}
/**
* Attempt to convert a web panel to an advanced panel.
* First check if the SRC matches a registered value.
* If so, call the advanced Panel Created method
* @param node DOM node representing the newly added web panel (child of .webpanel-stack)
* REMARK: Webview.src can add a trailing "/" to URLs
* REMARK: When initially created the webview may have no src,
* so we need to listen for the first src change
*/
function convertWebPanelToAdvancedPanel(node){
const addedWebview = node.querySelector("webview");
if(!addedWebview){
return;
}
if(!addedWebview.src){
addedWebview.addEventListener("contentload", webviewLoaded);
return;
}
for(const key in CUSTOM_PANELS){
const panel = CUSTOM_PANELS[key];
const expectedURL = panel.url;
if(addedWebview.src.startsWith(expectedURL)){
advancedPanelCreated(node, panel, key);
break;
}
}
}
/**
* Convert a web panel into an Advanced Panel™
* @param node the dom node added under web panel stack
* @param panel the panel object descriptor
* @param panelId the identifier (key) for the panel
* REMARK: Vivaldi can instantiate some new windows with an
* "empty" web panel containing nothing but the webview
* REMARK: Can't simply call node.innerHTML as otherwise the
* vivaldi UI will crash when attempting to hide the panel
* REMARK: Check that the panel isn't already an advanced panel
* before convert as this could be called after state change
* REMARK: it may take a while for vivaldi to update the title of
* a panel, therefore after it is terminated, the title may
* change to aborted. Title changing should be briefly delayed
*/
function advancedPanelCreated(node, panel, panelID){
if(node.getAttribute("advancedPanel")){
return;
}
node.setAttribute("advancedPanel", "true");
node.querySelector("webview").terminate();
const newHTML = document.createElement("div");
newHTML.innerHTML = panel.initialHTML;
node.appendChild(newHTML);
node.id = panelID;
panel.module().onInit();
ADVANCED_PANEL_ACTIVATION.observe(node, {attributes: true, attributeFilter: ["class"]});
if(node.querySelector("header.webpanel-header")){
advancedPanelOpened(node);
setTimeout(() => {updateAdvancedPanelTitle(node);}, 500);
}
}
/**
* Observe attributes of an advanced panel to see when it becomes active
*/
const ADVANCED_PANEL_ACTIVATION = new MutationObserver(records => {
records.forEach(record => {
if(record.target.classList.contains("visible")){
advancedPanelOpened(record.target);
}
});
});
/**
* An advanced panel has been selected by the user and is now active
* @param node DOM node of the advancedpanel activated
*/
function advancedPanelOpened(node){
updateAdvancedPanelTitle(node);
const panel = CUSTOM_PANELS[node.id];
panel.module().onActivate();
}
/**
* Update the header title of a panel
* @param node DOM node of the advancedpanel activated
*/
function updateAdvancedPanelTitle(node){
const panel = CUSTOM_PANELS[node.id];
node.querySelector("header.webpanel-header h1").innerHTML = panel.title;
node.querySelector("header.webpanel-header h1").title = panel.title;
}
/**
* Go through each advanced panel descriptor and convert the associated button
*/
function convertWebPanelButtonstoAdvancedPanelButtons(){
for(const key in CUSTOM_PANELS){
const panel = CUSTOM_PANELS[key];
let switchBtn = document.querySelector(`#switch button[title~="${panel.url}"`);
if(!switchBtn){
switchBtn = document.querySelector(`#switch button[advancedPanelSwitch="${key}"`);
if(!switchBtn){
console.warn(`Failed to find button for ${panel.title}`);
continue;
}
}
convertSingleButton(switchBtn, panel, key);
}
}
/**
* Set the appropriate values to convert a regular web panel switch into an advanced one
* @param switchBtn DOM node for the #switch button being converted
* @param panel The Advanced panel object description
* @param id string id of the panel
* REMARK: Check that the button isn't already an advanced panel button
* before convert as this could be called after state change
*/
function convertSingleButton(switchBtn, panel, id){
if(switchBtn.getAttribute("advancedPanelSwitch")){
return;
}
switchBtn.title = panel.title;
switchBtn.innerHTML = panel.switch;
switchBtn.setAttribute("advancedPanelSwitch", id);
}
/**
* Observe web panel switches.
* REMARK: When one is added or removed, all of the web panels are recreated
*/
const WEB_SWITCH_OBSERVER = new MutationObserver(records => {
convertWebPanelButtonstoAdvancedPanelButtons();
listenForNewPanelsAndConvertIfNecessary();
});
/**
* Start observing for additions or removals of web panel switches
*/
function observePanelSwitchChildren(){
const panelSwitch = document.querySelector("#switch");
WEB_SWITCH_OBSERVER.observe(panelSwitch, {childList: true});
}
/**
* Initialise the mod. Checking to make sure that the relevant panel element exists first.
*/
function initMod(){
if(document.querySelector("#panels .webpanel-stack")){
observeUIState();
observePanelSwitchChildren();
convertWebPanelButtonstoAdvancedPanelButtons();
listenForNewPanelsAndConvertIfNecessary();
} else {
setTimeout(initMod, 500);
}
}
initMod();
})();