/** * @author Jeremy England * @license MIT * @description Adds tabs, views, and controls to specified containers in node.js electron. * @requires electron, jquery, color.js, electron-context-menu, url-regex * @see https://github.com/simply-coded/electron-navigation * @tutorial * Add these IDs to your html (containers don't have to be divs). * * * * Add these scripts to your html (at the end of the body tag). * * Add a theme file to your html (at the end of the head tag)(optional). * */ import WebviewTag = Electron.WebviewTag; /** * DEPENDENCIES */ var jq = require('jquery'); var Color = require('color.js'); var urlRegex = require('url-regex'); const contextMenu = require('electron-context-menu') var globalCloseableTabsOverride; /** * OBJECT */ function Navigation(options) { /** * OPTIONS */ var defaults = { showBackButton: true, showForwardButton: true, showReloadButton: true, showUrlBar: true, showAddTabButton: true, closableTabs: true, verticalTabs: false, defaultFavicons: false, newTabCallback: null, changeTabCallback: null, newTabParams: null }; options = options ? Object.assign(defaults,options) : defaults; /** * GLOBALS & ICONS */ globalCloseableTabsOverride = options.closableTabs; const NAV = this; this.newTabCallback = options.newTabCallback; this.changeTabCallback = options.changeTabCallback; this.SESSION_ID = 1; this.SVG_BACK = ''; this.SVG_FORWARD = ''; this.SVG_RELOAD = ''; this.SVG_FAVICON = ''; this.SVG_ADD = ''; this.SVG_CLEAR = ''; /** * ADD ELEMENTS */ if (options.showBackButton) { jq('#nav-body-ctrls').append('' + this.SVG_BACK + ''); } if (options.showForwardButton) { jq('#nav-body-ctrls').append('' + this.SVG_FORWARD + ''); } if (options.showReloadButton) { jq('#nav-body-ctrls').append('' + this.SVG_RELOAD + ''); } if (options.showUrlBar) { jq('#nav-body-ctrls').append('') } if (options.showAddTabButton) { jq('#nav-body-tabs').append('' + this.SVG_ADD + ''); } /** * ADD CORE STYLE */ if (options.verticalTabs) { jq('head').append(''); } else { jq('head').append(''); } /** * EVENTS */ // // switch active view and tab on click // jq('#nav-body-tabs').on('click', '.nav-tabs-tab', function () { jq('.nav-tabs-tab, .nav-views-view').removeClass('active'); var sessionID = jq(this).data('session'); jq('.nav-tabs-tab, .nav-views-view') .filter('[data-session="' + sessionID + '"]') .addClass('active'); var session = jq('.nav-views-view[data-session="' + sessionID + '"]')[0]; (NAV.changeTabCallback || (() => {}))(session); NAV._updateUrl((session as WebviewTag).getURL()); NAV._updateCtrls(); // // close tab and view // }).on('click', '.nav-tabs-close', function() { var sessionID = jq(this).parent('.nav-tabs-tab').data('session'); var session = jq('.nav-tabs-tab, .nav-views-view').filter('[data-session="' + sessionID + '"]'); if (session.hasClass('active')) { if (session.next('.nav-tabs-tab').length) { session.next().addClass('active'); (NAV.changeTabCallback || (() => {}))(session.next()[1]); } else { session.prev().addClass('active'); (NAV.changeTabCallback || (() => {}))(session.prev()[1]); } } session.remove(); NAV._updateUrl(); NAV._updateCtrls(); return false; }); // // add a tab, default to google.com // jq('#nav-body-tabs').on('click', '#nav-tabs-add', function () { let params; if(typeof options.newTabParams === "function"){ params = options.newTabParams(); } else if(options.newTabParams instanceof Array){ params = options.newTabParams } else { params = ['http://www.google.com/', { close: options.closableTabs, icon: NAV.TAB_ICON }]; } NAV.newTab(...params); }); // // go back // jq('#nav-body-ctrls').on('click', '#nav-ctrls-back', function () { NAV.back(); }); // // go forward // jq('#nav-body-ctrls').on('click', '#nav-ctrls-forward', function () { NAV.forward(); }); // // reload page // jq('#nav-body-ctrls').on('click', '#nav-ctrls-reload', function () { if (jq(this).find('#nav-ready').length) { NAV.reload(); } else { NAV.stop(); } }); // // highlight address input text on first select // jq('#nav-ctrls-url').on('focus', function (e) { jq(this) .one('mouseup', function () { jq(this).select(); return false; }) .select(); }); // // load or search address on enter / shift+enter // jq('#nav-ctrls-url').keyup(function (this: HTMLInputElement, e) { if (e.keyCode == 13) { if (e.shiftKey) { NAV.newTab(this.value, { close: options.closableTabs, icon: NAV.TAB_ICON }); } else { if (jq('.nav-tabs-tab').length) { NAV.changeTab(this.value); } else { NAV.newTab(this.value, { close: options.closableTabs, icon: NAV.TAB_ICON }); } } } }); /** * FUNCTIONS */ // // update controls like back, forward, etc... // this._updateCtrls = function () { let webview = jq('.nav-views-view.active')[0] as WebviewTag; if (!webview) { jq('#nav-ctrls-back').addClass('disabled'); jq('#nav-ctrls-forward').addClass('disabled'); jq('#nav-ctrls-reload').html(this.SVG_RELOAD).addClass('disabled'); return; } if (webview.canGoBack()) { jq('#nav-ctrls-back').removeClass('disabled'); } else { jq('#nav-ctrls-back').addClass('disabled'); } if (webview.canGoForward()) { jq('#nav-ctrls-forward').removeClass('disabled'); } else { jq('#nav-ctrls-forward').addClass('disabled'); } if (webview.isLoading()) { this._loading(); } else { this._stopLoading(); } if (webview.getAttribute('data-readonly') == 'true') { jq('#nav-ctrls-url').attr('readonly', 'readonly'); } else { jq('#nav-ctrls-url').removeAttr('readonly'); } } //:_updateCtrls() // // start loading animations // this._loading = function (tab) { tab = tab || null; if (tab == null) { tab = jq('.nav-tabs-tab.active'); } tab.find('.nav-tabs-favicon').css('animation', 'nav-spin 2s linear infinite'); jq('#nav-ctrls-reload').html(this.SVG_CLEAR); } //:_loading() // // stop loading animations // this._stopLoading = function (tab) { tab = tab || null; if (tab == null) { tab = jq('.nav-tabs-tab.active'); } tab.find('.nav-tabs-favicon').css('animation', ''); jq('#nav-ctrls-reload').html(this.SVG_RELOAD); } //:_stopLoading() // // auto add http protocol to url input or do a search // this._purifyUrl = function (url) { if (urlRegex({ strict: false, exact: true }).test(url)) { url = (url.match(/^https?:\/\/.*/)) ? url : 'http://' + url; } else { url = (!url.match(/^[a-zA-Z]+:\/\//)) ? 'https://www.google.com/search?q=' + url.replace(' ', '+') : url; } return url; } //:_purifyUrl() // // set the color of the tab based on the favicon // this._setTabColor = function (url, currtab) { const getHexColor = new Color(url, { amount: 1, format: 'hex' }); getHexColor.mostUsed(result => { currtab.find('.nav-tabs-favicon svg').attr('fill', result); }); } //:_setTabColor() // // add event listeners to current webview // this._addEvents = function (sessionID, options) { let currtab = jq('.nav-tabs-tab[data-session="' + sessionID + '"]'); let webview = jq('.nav-views-view[data-session="' + sessionID + '"]') as JQuery; webview.on('dom-ready', function () { if (options.contextMenu) { contextMenu({ window: webview[0], labels: { cut: 'Cut', copy: 'Copy', paste: 'Paste', save: 'Save', copyLink: 'Copy Link', inspect: 'Inspect' } }); } }); webview.on('page-title-updated', function () { if (options.title == 'default') { currtab.find('.nav-tabs-title').text(webview[0].getTitle()); currtab.find('.nav-tabs-title').attr('title', webview[0].getTitle()); } }); webview.on('did-start-loading', function () { NAV._loading(currtab); }); webview.on('did-stop-loading', function () { NAV._stopLoading(currtab); }); webview.on('enter-html-full-screen', function () { jq('.nav-views-view.active').siblings().not('script').hide(); jq('.nav-views-view.active').parents().not('script').siblings().hide(); }); webview.on('leave-html-full-screen', function () { jq('.nav-views-view.active').siblings().not('script').show(); jq('.nav-views-view.active').parents().siblings().not('script').show(); }); webview.on('load-commit', function () { NAV._updateCtrls(); }); webview[0].addEventListener('did-navigate', (res) => { if(currtab[0] === jq('.nav-tabs-tab.active')[0]) NAV._updateUrl(res.url); }); webview[0].addEventListener('did-fail-load', (res) => { if(currtab[0] === jq('.nav-tabs-tab.active')[0]) NAV._updateUrl(res.validatedURL); }); webview[0].addEventListener('did-navigate-in-page', (res) => { if(currtab[0] === jq('.nav-tabs-tab.active')[0]) NAV._updateUrl(res.url); }); webview[0].addEventListener("new-window", (res) => { if (!(options.newWindowFrameNameBlacklistExpression instanceof RegExp && options.newWindowFrameNameBlacklistExpression.test(res.frameName))) { NAV.newTab(res.url, { icon: NAV.TAB_ICON }); } }); webview[0].addEventListener('page-favicon-updated', (res) => { currtab.find('.nav-tabs-favicon').replaceWith(jq('')); }); webview[0].addEventListener('did-fail-load', (res) => { if (res.validatedURL == jq('#nav-ctrls-url').val() && res.errorCode != -3) { this.executeJavaScript('document.body.innerHTML=' + '
' + '

Oops, this page failed to load correctly.

' + '

ERROR [ ' + res.errorCode + ', ' + res.errorDescription + ' ]

' + '

' + '

Try this

' + '
  • Check your spelling - "' + res.validatedURL + '".

  • ' + '
  • Refresh the page.

  • ' + '
  • Perform a search instead.

  • ' + '
    ' ); } }); return webview[0]; } //:_addEvents() // // update #nav-ctrls-url to given url or active tab's url // this._updateUrl = function (url) { url = url || null; let urlInput = jq('#nav-ctrls-url'); if (url == null) { if (jq('.nav-views-view').length) { url = (jq('.nav-views-view.active')[0] as WebviewTag).getURL(); } else { url = ''; } } urlInput.off('blur'); if (!urlInput.is(':focus')) { urlInput.prop('value', url); urlInput.data('last', url); } else { urlInput.on('blur', function () { // if url not edited if (urlInput.val() == urlInput.data('last')) { urlInput.prop('value', url); urlInput.data('last', url); } urlInput.off('blur'); }); } } //:_updateUrl() } //:Navigation() /** * PROTOTYPES */ // // create a new tab and view with an url and optional id // Navigation.prototype.newTab = function (url, options) { var defaults = { id: null, // null, 'yourIdHere' node: false, webviewAttributes: {}, icon: "clean", // 'default', 'clean', 'c:\location\to\image.png' title: "default", // 'default', 'your title here' close: true, readonlyUrl: false, contextMenu: true, newTabCallback: this.newTabCallback, changeTabCallback: this.changeTabCallback } options = options ? Object.assign(defaults,options) : defaults; if(typeof options.newTabCallback === "function"){ let result = options.newTabCallback(url, options); if(!result){ return null; } if(result.url){ url = result.url; } if(result.options){ options = result.options; } if(typeof result.postTabOpenCallback === "function"){ options.postTabOpenCallback = result.postTabOpenCallback; } } // validate options.id jq('.nav-tabs-tab, .nav-views-view').removeClass('active'); if (jq('#' + options.id).length) { console.log('ERROR[electron-navigation][func "newTab();"]: The ID "' + options.id + '" already exists. Please use another one.'); return false; } if (!(/^[A-Za-z]+[\w\-\:\.]*jq/.test(options.id))) { console.log('ERROR[electron-navigation][func "newTab();"]: The ID "' + options.id + '" is not valid. Please use another one.'); return false; } // build tab var tab = ''; // favicon if (options.icon == 'clean') { tab += '' + this.SVG_FAVICON + ''; } else if (options.icon === 'default') { tab += ''; } else { tab += ''; } // title if (options.title == 'default') { tab += ' . . . '; } else { tab += '' + options.title + ''; } // close if (options.close && globalCloseableTabsOverride) { tab += '' + this.SVG_CLEAR + ''; } // finish tab tab += ''; // add tab to correct position if (jq('#nav-body-tabs').has('#nav-tabs-add').length) { jq('#nav-tabs-add').before(tab); } else { jq('#nav-body-tabs').append(tab); } // add webview let composedWebviewTag = ` { composedWebviewTag += ` jq{key}="jq{options.webviewAttributes[key]}"`; }); } jq('#nav-body-views').append(`jq{composedWebviewTag}>`); // enable reload button jq('#nav-ctrls-reload').removeClass('disabled'); // update url and add events this._updateUrl(this._purifyUrl(url)); let newWebview = this._addEvents(this.SESSION_ID++, options); if(typeof options.postTabOpenCallback === "function"){ options.postTabOpenCallback(newWebview) } (this.changeTabCallback || (() => {}))(newWebview); return newWebview; } //:newTab() // // change current or specified tab and view // Navigation.prototype.changeTab = function (url, id) { id = id || null; if (id == null) { jq('.nav-views-view.active').attr('src', this._purifyUrl(url)); } else { if (jq('#' + id).length) { jq('#' + id).attr('src', this._purifyUrl(url)); } else { console.log('ERROR[electron-navigation][func "changeTab();"]: Cannot find the ID "' + id + '"'); } } } //:changeTab() // // close current or specified tab and view // Navigation.prototype.closeTab = function (id) { id = id || null; var session; if (id == null) { session = jq('.nav-tabs-tab.active, .nav-views-view.active'); } else { if (jq('#' + id).length) { var sessionID = jq('#' + id).data('session'); session = jq('.nav-tabs-tab, .nav-views-view').filter('[data-session="' + sessionID + '"]'); } else { console.log('ERROR[electron-navigation][func "closeTab();"]: Cannot find the ID "' + id + '"'); return false; } } if (session.next('.nav-tabs-tab').length) { session.next().addClass('active'); (this.changeTabCallback || (() => {}))(session.next()[1]); } else { session.prev().addClass('active'); (this.changeTabCallback || (() => {}))(session.prev()[1]); } session.remove(); this._updateUrl(); this._updateCtrls(); } //:closeTab() // // go back on current or specified view // Navigation.prototype.back = function (id) { id = id || null; if (id == null) { (jq('.nav-views-view.active')[0] as WebviewTag).goBack(); } else { if (jq('#' + id).length) { (jq('#' + id)[0] as WebviewTag).goBack(); } else { console.log('ERROR[electron-navigation][func "back();"]: Cannot find the ID "' + id + '"'); } } } //:back() // // go forward on current or specified view // Navigation.prototype.forward = function (id) { id = id || null; if (id == null) { (jq('.nav-views-view.active')[0] as WebviewTag).goForward(); } else { if (jq('#' + id).length) { (jq('#' + id)[0] as WebviewTag).goForward(); } else { console.log('ERROR[electron-navigation][func "forward();"]: Cannot find the ID "' + id + '"'); } } } //:forward() // // reload current or specified view // Navigation.prototype.reload = function (id) { id = id || null; if (id == null) { (jq('.nav-views-view.active')[0] as WebviewTag).reload(); } else { if (jq('#' + id).length) { (jq('#' + id)[0] as WebviewTag).reload(); } else { console.log('ERROR[electron-navigation][func "reload();"]: Cannot find the ID "' + id + '"'); } } } //:reload() // // stop loading current or specified view // Navigation.prototype.stop = function (id) { id = id || null; if (id == null) { (jq('.nav-views-view.active')[0] as WebviewTag).stop(); } else { if (jq('#' + id).length) { (jq('#' + id)[0] as WebviewTag).stop(); } else { console.log('ERROR[electron-navigation][func "stop();"]: Cannot find the ID "' + id + '"'); } } } //:stop() // // listen for a message from webview // Navigation.prototype.listen = function (id, callback) { let webview = null; //check id if (jq('#' + id).length) { webview = document.getElementById(id); } else { console.log('ERROR[electron-navigation][func "listen();"]: Cannot find the ID "' + id + '"'); } // listen for message if (webview != null) { try { webview.addEventListener('ipc-message', (event) => { callback(event.channel, event.args, webview); }); } catch (e) { webview.addEventListener("dom-ready", function (event) { webview.addEventListener('ipc-message', (event) => { callback(event.channel, event.args, webview); }); }); } } } //:listen() // // send message to webview // Navigation.prototype.send = function (id, channel, args) { let webview = null; // check id if (jq('#' + id).length) { webview = document.getElementById(id); } else { console.log('ERROR[electron-navigation][func "send();"]: Cannot find the ID "' + id + '"'); } // send a message if (webview != null) { try { webview.send(channel, args); } catch (e) { webview.addEventListener("dom-ready", function (event) { webview.send(channel, args); }); } } } //:send() // // open developer tools of current or ID'd webview // Navigation.prototype.openDevTools = function (id) { id = id || null; let webview = null; // check id if (id == null) { webview = jq('.nav-views-view.active')[0]; } else { if (jq('#' + id).length) { webview = document.getElementById(id); } else { console.log('ERROR[electron-navigation][func "openDevTools();"]: Cannot find the ID "' + id + '"'); } } // open dev tools if (webview != null) { try { webview.openDevTools(); } catch (e) { webview.addEventListener("dom-ready", function (event) { webview.openDevTools(); }); } } } //:openDevTools() // // print current or specified tab and view // Navigation.prototype.printTab = function (id, opts) { id = id || null let webview = null // check id if (id == null) { webview = jq('.nav-views-view.active')[0] } else { if (jq('#' + id).length) { webview = document.getElementById(id) } else { console.log('ERROR[electron-navigation][func "printTab();"]: Cannot find the ID "' + id + '"') } } // print if (webview != null) { webview.print(opts || {}); } } //:nextTab() // // toggle next available tab // Navigation.prototype.nextTab = function () { var tabs = jq('.nav-tabs-tab').toArray(); var activeTabIndex = tabs.indexOf(jq('.nav-tabs-tab.active')[0]); var nexti = activeTabIndex + 1; if (nexti > tabs.length - 1) nexti = 0; jq(jq('.nav-tabs-tab')[nexti]).trigger('click'); return false } //:nextTab() //:prevTab() // // toggle previous available tab // Navigation.prototype.prevTab = function () { var tabs = jq('.nav-tabs-tab').toArray(); var activeTabIndex = tabs.indexOf(jq('.nav-tabs-tab.active')[0]); var nexti = activeTabIndex - 1; if (nexti < 0) nexti = tabs.length - 1; jq(jq('.nav-tabs-tab')[nexti]).trigger('click'); return false } //:prevTab() // go to a tab by index or keyword // Navigation.prototype.goToTab = function (index) { let jqactiveTabAndView = jq('#nav-body-tabs .nav-tabs-tab.active, #nav-body-views .nav-views-view.active'); let jqtabAndViewToActivate; if (index == 'previous') { jqtabAndViewToActivate = jqactiveTabAndView.prev('#nav-body-tabs .nav-tabs-tab, #nav-body-views .nav-views-view'); } else if (index == 'next') { jqtabAndViewToActivate = jqactiveTabAndView.next('#nav-body-tabs .nav-tabs-tab, #nav-body-views .nav-views-view'); } else if (index == 'last') { jqtabAndViewToActivate = jq('#nav-body-tabs .nav-tabs-tab:last-of-type, #nav-body-views .nav-views-view:last-of-type'); } else { jqtabAndViewToActivate = jq('#nav-body-tabs .nav-tabs-tab:nth-of-type(' + index + '), #nav-body-views .nav-views-view:nth-of-type(' + index + ')'); } if (jqtabAndViewToActivate.length) { jq('#nav-ctrls-url').blur(); jqactiveTabAndView.removeClass('active'); jqtabAndViewToActivate.addClass('active'); this._updateUrl(); this._updateCtrls(); } } //:goToTab() // go to a tab by id of the webview tag Navigation.prototype.goToTabByWebviewId = function(id){ const webviews = document.querySelectorAll("webview.nav-views-view"); for(let index in webviews){ if(webviews[index].id == id){ this.goToTab(+index + 1); return; } } } //:goToTabByWebviewId() /** * MODULE EXPORTS */ module.exports = Navigation;