diff --git a/.gitignore b/.gitignore index 3351d9d..7e78571 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,6 @@ typings/ # ignore generated contents- /doc/out/* **/thumbnails -**/thumbnail.png \ No newline at end of file +**/thumbnail.png +/site/dist +/site/__pycache__ \ No newline at end of file diff --git a/bin/browser.sh b/bin/browser.sh new file mode 100755 index 0000000..7fd9d1d --- /dev/null +++ b/bin/browser.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ "$1" = "dist" ] +then + if [ -z "$2" ] + then + npm run package-all + else + npm run package-app-$2 + fi +else + electron . ./lib/index.html +fi \ No newline at end of file diff --git a/browser/browser.html b/browser/browser.html new file mode 100644 index 0000000..e3bb99d --- /dev/null +++ b/browser/browser.html @@ -0,0 +1,289 @@ + + + + + + + + Browser + + + + + +
+ + + + diff --git a/browser/carlo.js b/browser/carlo.js new file mode 100644 index 0000000..8cee63a --- /dev/null +++ b/browser/carlo.js @@ -0,0 +1,49 @@ +const carlo = require('carlo'); +const fse = require('fs-extra'); +const urlExists = require('url-exists'); + +// command line arguments +let path = 'index.html' +process.argv.forEach((value, index, array) => { + if (index === 2) { + path = value + } +}); + +(async () => { + // Launch the browser. + const opts = {} + + // Set path to custom chrome + const chrome = `${__dirname}/../chrome/chrome.exe` + if (fse.pathExistsSync(chrome)) { + opts.executablePath = chrome + } + + // Launch app + const app = await carlo.launch(opts) + + // Terminate Node.js process on app window closing. + app.on('exit', () => process.exit()) + + // Tell carlo where your web files are located. + app.serveFolder(`${__dirname}/../`) + + // Check if URL exists + urlExists('https://localhost:8443', async (error, exists) => { + + if (exists) { + console.info('Serve files via server') + app.serveOrigin('https://localhost:8443') // Optional + } else { + console.info('Serve files from file system') + } + + // Expose 'env' function in the web environment. + await app.exposeFunction('env', _ => process.env) + + // Navigate to the main page of your app. + console.info('Starting carlo with', path) + await app.load(path) + }) +})() diff --git a/browser/i18n.js b/browser/i18n.js new file mode 100644 index 0000000..f262f8c --- /dev/null +++ b/browser/i18n.js @@ -0,0 +1,24 @@ +const path = require('path') +const electron = require('electron') +const fs = require('fs') +let loadedLanguage +let app = electron.app ? electron.app : electron.remote.app + +module.exports = i18n + +function i18n() { + if (fs.existsSync(path.join(__dirname, 'i18n', app.getLocale() + '.js'))) { + loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'i18n', app.getLocale() + '.js'), 'utf8')) + } + else { + loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'i18n', 'en.js'), 'utf8')) + } +} + +i18n.prototype.__ = function(phrase) { + let translation = loadedLanguage[phrase] + if (translation === undefined) { + translation = phrase + } + return translation +} \ No newline at end of file diff --git a/browser/i18n/de.js b/browser/i18n/de.js new file mode 100644 index 0000000..2af6e0c --- /dev/null +++ b/browser/i18n/de.js @@ -0,0 +1,61 @@ +{ + "edit": "Bearbeiten", + "undo": "Widerrufen", + "redo": "Wiederholen", + "cut": "Ausschneiden", + "copy": "Kopieren", + "paste": "Einsetzen", + "pasteandmatchstyle": "Einsetzen und Stil anpassen", + "delete": "Löschen", + "selectall": "Alles auswählen", + "view": "Darstellung", + "reload": "Seite neu laden", + "forcereload": "Cache löschen und Seite neu laden", + "resetzoom": "Originalgröße", + "zoomin": "Vergrößern", + "zoomout": "Verkleinern", + "togglefullscreen": "Vollbildmodus umschalten", + "minimalpad": "Minimal-Pad", + "multiuserbrowser": "Mehrbenutzer-Browser", + "history": "Verlauf", + "back": "Zurück", + "forward": "Vorwärts", + "home": "Startseite", + "recentlyvisited": "Kürzlich besucht", + "bookmarks": "Lesezeichen", + "localfilesystem": "Lokales Dateisystem", + "testframes": "Testseiten", + "develop": "Entwickler", + "toggledevelopertools": "Webinformationen umschalten", + "openprocessmonitor": "Prozessmonitor öffnen", + "selectfolder": "Datenverzeichnis auswählen...", + "selectfolder.noadmin.ok": "OK", + "selectfolder.noadmin.message": "Keine ausreichenden Berechtigungen", + "selectfolder.noadmin.detail": "Um das Datenverzeichnis zu ändern, muss der IWM Browser mit Administrator-Berechtigungen gestartet werden.", + "selectfolder.warning.next": "Weiter", + "selectfolder.warning.cancel": "Abbrechen", + "selectfolder.warning.message": "Datenverzeichnis vorhanden", + "selectfolder.warning.detail": "Ihr IWM Browser besitzt bereits ein (verlinktes) Datenverzeichnis. Wenn Sie fortfahren, wird das alte Verzeichnis gesichert und ein neues wird erstellt.", + "selectfolder.select.title": "Datenverzeichnis wählen", + "selectfolder.select.buttonLabel": "Auswählen", + "selectfolder.samefolder.ok": "OK", + "selectfolder.samefolder.message": "Ungültiges Datenverzeichnis", + "selectfolder.samefolder.detail.same": "Das alte Datenverzeichnis darf nicht als neues Verzeichnis ausgewählt werden.", + "selectfolder.samefolder.detail.within": "Das neue Datenverzeichnis darf sich nicht innerhalb des alten Verzeichnisses befinden.", + "selectfolder.info.ok": "OK", + "selectfolder.info.message": "Link auf Datenverzeichnis erstellt", + "selectfolder.info.detail": "Der IWM Browser verwendet nun den Ordner \"${0}\" als neues Datenverzeichnis.", + "startserver": "Starte Server", + "stopserver": "Stoppe Server", + "runloadtests": "Starte Ladetests", + "window": "Fenster", + "close": "Fenster schließen", + "minimize": "Im Dock ablegen", + "zoom": "Zoomen", + "front": "Alle nach vorne bringen", + "screenshot": "Bildschirmfoto erstellen", + "help": "Hilfe", + "iwm": "Leibniz-Institut für Wissensmedien", + "about": "Über IWM Browser", + "quit": "IWM Browser beenden" +} diff --git a/browser/i18n/en.js b/browser/i18n/en.js new file mode 100644 index 0000000..8e9163d --- /dev/null +++ b/browser/i18n/en.js @@ -0,0 +1,61 @@ +{ + "edit": "Edit", + "undo": "Undo", + "redo": "Redo", + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "pasteandmatchstyle": "Paste and Match Style", + "delete": "Delete", + "selectall": "Select all", + "view": "View", + "reload": "Reload", + "forcereload": "Force Reload", + "resetzoom": "Actual size", + "zoomin": "Zoom in", + "zoomout": "Zoom out", + "togglefullscreen": "Toggle Full Screen", + "minimalpad": "Minimal Pad", + "multiuserbrowser": "Multi-User Browser", + "history": "History", + "back": "Back", + "forward": "Forward", + "home": "Home", + "recentlyvisited": "Recently Visited", + "bookmarks": "Bookmarks", + "localfilesystem": "Local Filesystem", + "testframes": "Test Frames", + "develop": "Develop", + "toggledevelopertools": "Toggle Developer Tools", + "openprocessmonitor": "Open Process Monitor", + "selectfolder": "Select data folder...", + "selectfolder.noadmin.ok": "OK", + "selectfolder.noadmin.message": "Insufficient permissions", + "selectfolder.noadmin.detail": "To change the data directory, the IWM Browser must be started with administrator privileges.", + "selectfolder.warning.next": "Next", + "selectfolder.warning.cancel": "Cancel", + "selectfolder.warning.message": "Data folder exists", + "selectfolder.warning.detail": "Your IWM Browser already has a (linked) data directory. If you continue, the old directory is backed up and a new one is created.", + "selectfolder.select.title": "Select data folder", + "selectfolder.select.buttonLabel": "Select", + "selectfolder.samefolder.ok": "OK", + "selectfolder.samefolder.message": "Invalid data folder", + "selectfolder.samefolder.detail.same": "The old data directory cannot be selected as the new directory.", + "selectfolder.samefolder.detail.within": "The new data directory cannot be inside the old directory.", + "selectfolder.info.ok": "OK", + "selectfolder.info.message": "Created link to data folder", + "selectfolder.info.detail": "The IWM Browser now uses the folder \"${0}\" as the new data folder.", + "startserver": "Start Server", + "stopserver": "Stop Server", + "runloadtests": "Run Load Tests", + "window": "Window", + "close": "Close", + "minimize": "Minimize", + "zoom": "Zoom", + "front": "Bring All to Front", + "screenshot": "Make Screenshot", + "help": "Help", + "iwm": "Leibniz-Institut für Wissensmedien", + "about": "About IWM Browser", + "quit": "Quit IWM Browser" +} diff --git a/browser/main.js b/browser/main.js new file mode 100644 index 0000000..8b9bbfb --- /dev/null +++ b/browser/main.js @@ -0,0 +1,578 @@ +/* globals require, __dirname, process */ +/*eslint no-console: ["error", { allow: ["log"] }]*/ + +const { app, BrowserWindow, BrowserView, ipcMain, dialog, shell } = require('electron') +const electronLocalshortcut = require('electron-localshortcut') +//const electron = require('electron') +const os = require('os') +const fs = require('fs') +const path = require('path') +const { URL } = require('url') +const Store = require('./store.js') +const { prettyPrint } = require('html') + +// Use this constant to start the application in kiosk-mode or in development mode +const DEVELOPMENT = true +// true: Dev-Tools are open +// false (KIOSK-Mode): No application switcher, no menu, no taskbar (or dock on a mac), shortcuts are working + +global.multiUserMode = true +global.errorCount = 0 +global.stopTestsOnError = false +global.jsonData = { value: null } +// UO: Experimental feature using Native Windows +global.useBrowserView = false +global.useMinimalPad = true +global.menu = null +global.observeTraffic = false + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let win +let browsers = new Map() // url: BrowserWindow + +const store = new Store({ + // We'll call our data file 'user-preferences' + configName: 'user-preferences', + defaults: { + url: `file://${__dirname}/index.html`, + devTools: DEVELOPMENT, + multiUserBrowser: true + } +}) + +function createWindow() { + + if (global.observeTraffic) { + const {session} = require('electron') + session.defaultSession.webRequest.onCompleted((details) => { + console.log("onCompleted", details.url) + }) + } + let { screen } = require('electron') + + let bounds = store.get('storedBounds') + ? store.get('storedBounds') + : screen.getPrimaryDisplay().bounds + + // let displays = screen.getAllDisplays() + // let externalDisplay = null + + // externalDisplay = displays[displays.length-1] + // const {width, height} =displays[displays.length-1].workAreaSize + + // externalDisplay = displays[0] + // const {width, height} =displays[0].workAreaSize + + win = new BrowserWindow({ + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + fullscreenable: true, + fullscreen: !DEVELOPMENT, + title: 'IWM Browser', + show: false, + kiosk: !DEVELOPMENT, + acceptFirstMouse: true, + webPreferences: { + webSecurity: false, + allowRunningInsecureContent: true, + nodeIntegration: true, + webviewTag: true, + nativeWindowOpen: true, + devTools: true, + preload: path.join(__dirname, './preload.js') + }, + icon: path.join(__dirname, 'assets/icons/png/64x64.png') + }) + + module.exports.win = win + + let url = store.get('url') + if (process.argv.length > 2) { + let path = process.argv[2] + url = `file://${__dirname}/../${path}` + console.log('Using process.argv[2]', url) + } + console.log('Using', url) + win.maximize() + + // BAD: All other methods don't work (like ensureFileSync, fileExists...) + try { + let settings = require('./settings.json') + console.log('Using settings', `file://${__dirname}/${settings.url}`) + win.loadURL(`file://${__dirname}/${settings.url}`) + } catch (ex) { + win.loadURL(url) + } + + const { webContents } = win + // + // if (process.platform === 'win32' && win.isKiosk()) { + // webContents.on('did-finish-load', function() { + // webContents.executeJavaScript('document.body.style.cursor = "none";') + // }) + // } + + // Add the app menu + let menu = require('./menu.js') + global.menu = menu + + // Add global shortcuts + // Esc quits the app + electronLocalshortcut.register('Esc', () => { + app.quit() + }) + + // Command (Mac) or Control (Win) + K toggles the Kiosk mode + electronLocalshortcut.register('CommandOrControl+K', () => { + if (win) { + win.setKiosk(!win.isKiosk()) + } + }) + + // Show if its ready. + win.once('ready-to-show', () => { + webContents.send('preparePads') + win.show() + }) + + // Clear cache + webContents.session.clearCache(() => console.log('Cache cleared')) + + // Open dev tools when in development mode + if (store.get('devTools')) { + webContents.openDevTools({ mode: 'right' }) + } else { + webContents.closeDevTools() + } + + webContents.on('devtools-opened', () => { + store.set('devTools', true) + }) + + webContents.on('devtools-closed', () => { + store.set('devTools', false) + }) + + webContents.on('did-navigate', (event, url) => { + menu.setHistoryStatus() + }) + + /* UO: At this point we have no access to the event or link position*/ + + webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => { + console.log('new-window', global.multiUserMode) + if (global.multiUserMode) { + event.preventDefault() + webContents.send('newPad', url, options.x, options.y) + } + }) + + // WORKAROUND: On windows, if the app was set to fullscreen, the menubar is not hidden + if (win.isKiosk()) { + win.setMenuBarVisibility(false) + } + + win.on('focus', event => { + menu.focus() + }) + + win.on('blur', event => { + menu.blur() + }) + + win.on('enter-full-screen', () => { + win.setMenuBarVisibility(false) + }) + + win.on('leave-full-screen', () => { + win.setMenuBarVisibility(true) + }) + + win.on('enter-html-full-screen', () => { + win.setMenuBarVisibility(false) + }) + + win.on('leave-html-full-screen', () => { + win.setMenuBarVisibility(true) + }) + win.on('close', () => { + store.set('storedBounds', win.getBounds()) + }) + + // Emitted when the window is closed. + win.on('closed', () => { + app.quit() + }) +} + +// When work makes progress, show the progress bar +function onProgress(progress) { + // Use values 0 to 1, or -1 to hide the progress bar + try { + win.setProgressBar(progress || -1) // Progress bar works on all platforms + } catch (e) { + if (DEVELOPMENT) console.log(e.message) + } +} + +function trySend(target, ...args) { + try { + target.send(...args) + } catch (e) { + if (DEVELOPMENT) console.log(e.message) + } +} + +function openBrowserView(url, x, y) { + const useMinBrowser = false // Change this to switch between Min and a custom browser + + const minURL = 'file:///Users/uo/devel/min/index.html' + const browserURL = `file://${__dirname}/browser.html` + let width = 640 + let height = 1200 + let [winWidth, winHeight] = win.getSize() + if (x + width > winWidth) { + x = winWidth - width + } + if (y + height > winHeight) { + y = winHeight - height + } + console.log('open browser view') + let browser = new BrowserWindow({ + x, + y, + width, + height, + minWidth: 320, + minHeight: 350, + titleBarStyle: useMinBrowser ? 'hidden-inset' : 'hidden', + frame: process.platform !== 'win32' + }) + let browserContents = browser.webContents + browser.setAlwaysOnTop(true) + if (useMinBrowser) { + browserContents.on('did-finish-load', event => { + console.log('did-finish-load', browserContents.getURL()) + + browserContents.executeJavaScript( + 'Object.values(window.webviews.elementMap).map(obj => obj.src)', + result => { + console.log( + 'window.webviews', + result, + url, + result.indexOf(url) + ) + if (result.indexOf(url) == -1) { + console.log('Adding tab') + browserContents.send('addTab', { url }) + } + } + ) + }) + browser.loadURL(minURL) + } else { + console.log('Loading', browserURL) + browser.loadURL(browserURL) + let view = new BrowserView({ + webPreferences: { + nodeIntegration: false, + devTools: true + } + }) + //browserContents.openDevTools({mode: 'right'}) + + browser.setBrowserView(view) + view.setBounds({ x: 0, y: 24, width: width, height: height - 24 }) + view.setAutoResize({ width: true, height: true }) + + let viewContents = view.webContents + let progress = 0 + + viewContents.on('page-title-set', event => { + console.log('page-title-set', event) + }) + viewContents.on('page-favicon-updated', (event, favicons) => { + //console.log("page-favicon-updated", event, favicons) + trySend(browserContents, 'favicons', favicons) + }) + + viewContents.on('did-start-loading', event => { + onProgress(0) + trySend(browserContents, 'progress', 0) + trySend(browserContents, 'did-start-loading') + //let senderURL = event.sender.getURL() || url + //console.log('did-start-loading', senderURL) + }) + viewContents.on( + 'did-get-response-details', + ( + event, + status, + newURL, + originalURL, + httpResponseCode, + requestMethod, + referrer, + headers, + resourceType + ) => { + trySend(browserContents, 'did-get-response-details', { + status, + newURL, + originalURL, + httpResponseCode, + requestMethod, + referrer, + headers, + resourceType + }) + progress += 0.01 + onProgress(progress) + trySend(browserContents, 'progress', progress) + //console.log('did-get-response-details', newURL) + } + ) + viewContents.on( + 'did-get-redirect-request', + ( + event, + oldURL, + newURL, + isMainFrame, + httpResponseCode, + requestMethod, + referrer, + headers + ) => { + trySend(browserContents, 'did-get-redirect-request', { + oldURL, + newURL, + isMainFrame, + httpResponseCode, + requestMethod, + referrer, + headers + }) + //console.log('did-get-redirect-request', newURL) + } + ) + viewContents.on('did-stop-loading', event => { + //console.log('did-stop-loading', event.sender.getURL()) + trySend(browserContents, 'did-stop-loading') + }) + viewContents.on('did-finish-load', event => { + //console.log('did-finish-load', event.sender.getURL()) + progress = 1 + onProgress(progress) + trySend(browserContents, 'progress', progress) + }) + viewContents.on('dom-ready', event => { + if (progress < 0.5) { + progress = 0.5 + onProgress(progress) + trySend(browserContents, 'progress', progress) + } + viewContents.executeJavaScript('document.title', result => { + trySend(browserContents, 'title', result) + }) + + //console.log('dom-ready', event.sender.getURL()) + }) + + viewContents.on('new-window', function (event, url) { + event.preventDefault() + console.log('new-window') + openBrowserView(url, x, y) + }) + + viewContents.loadURL(url) + } + browsers.set(url, browser) + browser.on('closed', e => { + for (let [url, browser] of browsers.entries()) { + if (browser == e.sender) { + browsers.delete(url) + console.log('removed browser view', url) + } + } + }) +} + +// UO: Experimental call. Opens a Min Browser window or a limited window with a browser view +ipcMain.on('loadBrowserView', (e, opts = {}) => { + let { url, x, y } = opts + openBrowserView(url, x, y) +}) + +ipcMain.on('multiUserMode', (e, opts = {}) => { + global.multiUserMode = opts +}) + +ipcMain.on('padContainerLoaded', e => { + win.webContents.send('padContainerAvailable') +}) + +ipcMain.on('createScreenshot', (e, opts = {}) => { + opts = Object.assign( + {}, + { + name: `iwmbrowser-${new Date() + .toISOString() + .replace(/:/g, '-')}.png`, + path: os.tmpdir() + }, + opts + ) + + win.webContents.capturePage(image => { + if (image) { + let file = path.join(opts.path, opts.name) + fs.writeFile(file, image.toPNG(), err => { + if (err) { + //throw err + } else { + console.log(`Screenshot saved: ${file}`) + } + }) + } + }) +}) + +ipcMain.on('directoryListing', (e, opts = {}) => { + let { directory, files, folders } = opts + console.log("directoryListing", opts) + try { + let listing = fs.readdirSync(directory) + let result = { directory, files: [], folders: [] } + for (let name of listing) { + if (name.startsWith('.')) + continue + let fullPath = path.join(directory, name) + let stat = fs.lstatSync(fullPath) + if (files && stat.isFile()) { + if (typeof files == 'string' && !files.endsWith(files)) + continue + result.files.push(name) + } + if (folders && stat.isDirectory()) + result.folders.push(name) + } + e.sender.send('directoryListing', result) + } catch (err) { + let args = { directory, errorMessage: err.message} + e.sender.send('directoryListingError', args) + } +}) + +ipcMain.on('createTextfile', (e, opts = {}) => { + opts = Object.assign( + {}, + { + name: `iwmbrowser-${new Date() + .toISOString() + .replace(/:/g, '-')}.txt`, + path: os.tmpdir(), + text: '' + }, + opts + ) + let file = path.join(opts.path, opts.name) + fs.writeFile(file, opts.text, err => { + if (err) { + //throw err + } else { + console.log(`Textfile saved: ${file}`) + } + }) +}) + +ipcMain.on('error', e => { + console.log('Received error notification') + global.errorCount += 1 +}) + +ipcMain.on('openExternal', (e, url=null) => { + console.log('Received openExternal', url) + if (url) { + shell.openExternal(url) + } +}) + +ipcMain.on('save', (e, opts = {}) => { + let { url, html, saveAs, action } = opts + // url must absolute URL + let urlObj = new URL(url) + let pathname = urlObj.pathname + if (saveAs) { + pathname = dialog.showSaveDialog(win, { title: 'Save as:', defaultPath: pathname }) + if (typeof pathname == 'undefined') + return + } + try { + console.log("Saving", pathname, action) + html = prettyPrint(html, { indent_size: 4 }); + fs.writeFileSync(pathname, html, 'utf-8') + if (saveAs) { + let normalized = pathname.replace(/\\/g, '/') + e.sender.send('savedAs', {url: `file://${normalized}`, action}) + } + } + catch (e) { + console.warn('Failed to save the file', pathname) + e.sender.send('saveFailed', pathname) + } + +}) + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', () => { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on( + 'select-client-certificate', + (event, webContents, url, list, callback) => { + console.log('select-client-certificate', url, list) + event.preventDefault() + ipc.once('client-certificate-selected', (event, item) => { + console.log('selected:', item) + callback(item) + }) + mainWindow.webContents.send('select-client-certificate', list) + } +) + +app.on( + 'certificate-error', + (event, webContents, url, error, certificate, callback) => { + console.log('certificate-error', url) + event.preventDefault() + const result = true // TODO: do real validation here + callback(result) + } +) + +app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (win === null) { + createWindow() + } +}) + +module.exports = { + store: store +} diff --git a/browser/menu.js b/browser/menu.js new file mode 100644 index 0000000..c20845b --- /dev/null +++ b/browser/menu.js @@ -0,0 +1,629 @@ +/* globals require, process */ +/*eslint no-console: ["error", { allow: ["log", "error"] }] */ + +const { Menu, app, shell, dialog } = require('electron') +const fs = require('fs') +const fse = require('fs-extra') +const os = require('os') +const path = require('path') +const { openProcessManager } = require('electron-process-manager') +const main = require('./main.js') + +let { thumbnail } = require('./utils.js') +const loadTests = require('./test.js') +const i18n = new (require('./i18n.js'))() + +function selectURL(url) { + url = url.replace(/\\/g, '/') + console.log('selectURL', url) + main.win.loadURL(url) + main.store.set('url', url) +} + +function findItems(key, value) { + let items = [] + + for (let i = 0; i < menu.items.length; i++) { + for (let j = 0; j < menu.items[i].submenu.items.length; j++) { + let item = menu.items[i].submenu.items[j] + if (item[key] === value) { + items.push(item) + } + } + } + + return items +} + +function findItem(key, value) { + return findItems(key, value)[0] +} + +function toggleBookmarks(bookmark) { + let items = findItems('class', 'bookmark') + + for (let i = 0; i < items.length; i++) { + items[i].checked = false + } + + bookmark.checked = true +} + +function checkBookmark(url) { + let items = findItems('url', url) + if (items.length === 1) { + toggleBookmarks(items[0]) + } +} + +function setHistoryStatus() { + const historyBack = findItem('id', 'history-back') + historyBack.enabled = main.win.webContents.canGoBack() + + const historyForward = findItem('id', 'history-forward') + historyForward.enabled = main.win.webContents.canGoForward() +} + +function showSelectDataFolderDialog(focusedWindow) { + dialog.showOpenDialog( + { + title: i18n.__('selectfolder.select.title'), + buttonLabel: i18n.__('selectfolder.select.buttonLabel'), + properties: ['openDirectory', 'createDirectory', 'noResolveAliases', 'treatPackageAsDirectory'] + }, + (filePaths) => { + if (filePaths && filePaths.length === 1) { + const varPath = path.join(__dirname, '../var') + + // Check if the same folder was used + if (filePaths[0].startsWith(varPath)) { + const same = filePaths[0] === varPath + + dialog.showMessageBox( + { + type: 'error', + icon: path.join(__dirname, '../assets/icons/png/512x512-empty.png'), + buttons: [i18n.__('selectfolder.samefolder.ok')], + defaultId: 0, + message: i18n.__('selectfolder.samefolder.message'), + detail: same + ? i18n.__('selectfolder.samefolder.detail.same') + : i18n.__('selectfolder.samefolder.detail.within'), + cancelId: 0 + }, + (response) => { + showSelectDataFolderDialog(focusedWindow) + } + ) + } else { + // Backup + if (fse.pathExistsSync(varPath)) { + const varPathBackup = findNextVarFolder() + // Rename old var folder or link + fse.renameSync(varPath, varPathBackup) + } else { + // BUG: Workaround because pathExistsSync return false on existing symbolic links with a missing target + fse.removeSync(varPath) + } + + // Add new symlink + main.store.set('dataFolder', filePaths[0]) + fs.symlinkSync(filePaths[0], varPath, 'dir') + + dialog.showMessageBox( + { + type: 'info', + icon: path.join(__dirname, '../assets/icons/png/link.png'), + buttons: [i18n.__('selectfolder.info.ok')], + defaultId: 0, + message: i18n.__('selectfolder.info.message'), + detail: i18n.__('selectfolder.info.detail').replace(/\$\{0\}/, filePaths[0]), + cancelId: 0 + }, + (response) => { + if (focusedWindow) focusedWindow.reload() + } + ) + } + } + } + ) +} + +function findNextVarFolder() { + let exists = true + let counter = 0 + + while (exists) { + counter++ + exists = fse.pathExistsSync(path.join(__dirname, `../var${counter}`)) + } + + return path.join(__dirname, `../var${counter}`) +} + +function showFolderBrowser(focusedWindow) { + const varPath = path.join(__dirname, '../var') + const varPathExists = fse.pathExistsSync(varPath) + if (varPathExists) { + dialog.showMessageBox( + { + type: 'warning', + icon: path.join(__dirname, '../assets/icons/png/512x512-empty.png'), + buttons: [i18n.__('selectfolder.warning.next'), i18n.__('selectfolder.warning.cancel')], + defaultId: 1, + message: i18n.__('selectfolder.warning.message'), + detail: i18n.__('selectfolder.warning.detail'), + cancelId: 1 + }, + (response) => { + if (response === 0) { + showSelectDataFolderDialog(focusedWindow) + } + } + ) + } else { + showSelectDataFolderDialog(focusedWindow) + } +} + +const template = [ + { + label: i18n.__('edit'), + submenu: [ + { + role: 'undo', + label: i18n.__('undo') + }, + { + role: 'redo', + label: i18n.__('redo') + }, + { + type: 'separator' + }, + { + role: 'cut', + label: i18n.__('cut') + }, + { + role: 'copy', + label: i18n.__('copy') + }, + { + role: 'paste', + label: i18n.__('paste') + }, + { + role: 'pasteandmatchstyle', + label: i18n.__('pasteandmatchstyle') + }, + { + role: 'delete', + label: i18n.__('delete') + }, + { + role: 'selectall', + label: i18n.__('selectall') + } + ] + }, + { + label: i18n.__('view'), + submenu: [ + { + label: i18n.__('reload'), + accelerator: 'CmdOrCtrl+R', + click(item, focusedWindow) { + if (focusedWindow) { + focusedWindow.webContents.setVisualZoomLevelLimits(1, 1) + focusedWindow.reload() + } + } + }, + { + id: 'forcereload', + label: i18n.__('forcereload'), + accelerator: 'CmdOrCtrl+Shift+R', + click(item, focusedWindow) { + if (focusedWindow) { + focusedWindow.webContents.session.clearCache(() => console.log('Cache cleared')) + + focusedWindow.webContents.setVisualZoomLevelLimits(1, 1) + focusedWindow.reload() + } + } + }, + { + type: 'separator' + }, + { + role: 'resetzoom', + label: i18n.__('resetzoom') + }, + { + role: 'zoomin', + label: i18n.__('zoomin') + }, + { + role: 'zoomout', + label: i18n.__('zoomout') + }, + { + type: 'separator' + }, + { + id: 'togglefullscreen', + label: i18n.__('togglefullscreen'), + accelerator: process.platform === 'darwin' ? 'Cmd+Ctrl+F' : 'F11', + click(item, focusedWindow) { + if (focusedWindow) { + focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) + } + } + }, + { + type: 'separator' + }, + { + label: i18n.__('multiuserbrowser'), + accelerator: 'CmdOrCtrl+M', + type: 'checkbox', + checked: true, + click(item, focusedWindow) { + if (focusedWindow) { + main.store.set('multiUserBrowser', item.checked) + global.multiUserMode = item.checked + } + } + }, + { + label: i18n.__('minimalpad'), + accelerator: 'CmdOrCtrl+p', + type: 'checkbox', + checked: true, + click(item, focusedWindow) { + if (focusedWindow) { + main.store.set('minimalPad', item.checked) + global.useMinimalPad = item.checked + } + } + } + ] + }, + { + label: i18n.__('history'), + submenu: [ + { + id: 'history-back', + label: i18n.__('back'), + accelerator: 'CmdOrCtrl+Left', + click(item, focusedWindow) { + main.win.webContents.goBack() + } + }, + { + id: 'history-forward', + label: i18n.__('forward'), + accelerator: 'CmdOrCtrl+Right', + click(item, focusedWindow) { + main.win.webContents.goForward() + } + }, + { + label: i18n.__('home'), + accelerator: 'CmdOrCtrl+Up', + click(item, focusedWindow) { + main.win.webContents.goToIndex(0) + } + }, + { + type: 'separator' + }, + { + label: i18n.__('recentlyvisited'), + enabled: false + } + ] + }, + { + label: i18n.__('bookmarks'), + submenu: [ + { + label: i18n.__('localfilesystem'), + class: 'bookmark', + type: 'checkbox', + url: `file://${__dirname}/../index.html`, + accelerator: 'CmdOrCtrl+L', + click(item, focusedWindow) { + selectURL(item.url) + toggleBookmarks(item) + } + }, + { + label: i18n.__('testframes'), + class: 'bookmark', + type: 'checkbox', + url: `file://${__dirname}/../index.html?test`, + accelerator: 'CmdOrCtrl+T', + click(item, focusedWindow) { + selectURL(item.url) + toggleBookmarks(item) + } + }, + { + type: 'separator' + }, + { + id: 'localhost', + label: 'https://localhost:8443', + class: 'bookmark', + type: 'checkbox', + enabled: false, + url: 'https://localhost:8443/index.html', + click(item, focusedWindow) { + selectURL(item.url) + toggleBookmarks(item) + } + }, + { + id: 'localhost', + label: 'https://localhost:3000', + class: 'bookmark', + type: 'checkbox', + enabled: true, + url: 'https://localhost:3000/index.html', + click(item, focusedWindow) { + selectURL(item.url) + toggleBookmarks(item) + } + }, + // { + // label: 'http://tornado.iwm-kmrc.de:8000', + // class: 'bookmark', + // type: 'checkbox', + // url: 'http://tornado.iwm-kmrc.de:8000/index.html', + // click(item, focusedWindow) { + // selectURL(item.url) + // toggleBookmarks(item) + // } + // }, + { + label: 'http://rousseau.iwm-kmrc.de/index.html', + class: 'bookmark', + type: 'checkbox', + url: 'http://rousseau.iwm-kmrc.de/index.html', + click(item, focusedWindow) { + selectURL(item.url) + toggleBookmarks(item) + } + } + ] + }, + { + label: i18n.__('develop'), + submenu: [ + { + id: 'toggledevelopertools', + label: i18n.__('toggledevelopertools'), + accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', + click(item, focusedWindow) { + if (focusedWindow) focusedWindow.webContents.toggleDevTools() + } + }, + { + label: i18n.__('openprocessmonitor'), + accelerator: process.platform === 'darwin' ? 'Alt+Command+P' : 'Ctrl+Shift+P', + click(item, focusedWindow) { + openProcessManager() + } + }, + { + type: 'separator' + }, + { + label: i18n.__('selectfolder'), + accelerator: process.platform === 'darwin' ? 'Alt+Command+D' : 'Ctrl+Shift+D', + click(item, focusedWindow) { + if (process.platform === 'win32') { + var exec = require('child_process').exec + exec('NET SESSION', function (err, so, se) { + const admin = se.length === 0 ? true : false + + if (admin) { + showFolderBrowser(focusedWindow) + } else { + dialog.showMessageBox({ + type: 'error', + icon: path.join(__dirname, '../assets/icons/png/512x512-empty.png'), + buttons: [i18n.__('selectfolder.noadmin.ok')], + message: i18n.__('selectfolder.noadmin.message'), + detail: i18n.__('selectfolder.noadmin.detail') + }) + } + }) + } else { + showFolderBrowser(focusedWindow) + } + } + }, + { + type: 'separator' + }, + { + id: 'startserver', + label: i18n.__('startserver'), + accelerator: process.platform === 'darwin' ? 'Alt+Command+S' : 'Ctrl+Shift+S', + click(item, focusedWindow) { + const { server } = require('../server/main.js') + server.start() + item.visible = false + findItem('id', 'stopserver').visible = true + findItem('id', 'localhost').enabled = true + } + }, + { + id: 'stopserver', + label: i18n.__('stopserver'), + accelerator: process.platform === 'darwin' ? 'Alt+Command+S' : 'Ctrl+Shift+S', + visible: false, + click(item, focusedWindow) { + const { server } = require('../server/main.js') + server.stop() + item.visible = false + findItem('id', 'startserver').visible = true + findItem('id', 'localhost').enabled = false + } + }, + { + type: 'separator' + }, + { + label: i18n.__('runloadtests'), + accelerator: process.platform === 'darwin' ? 'Alt+Command+L' : 'Ctrl+Shift+L', + click(item, focusedWindow) { + loadTests(focusedWindow) + } + }, + { + type: 'separator' + }, + { + label: 'Aktualisiere Tüsch POIs', + click(item, focusedWindow) { + const UpdatePOI = require('../dev/tuesch/bin/menu/update-pois.js') + UpdatePOI.update('./dev/tuesch') + } + } + ] + }, + { + role: 'window', + label: i18n.__('window'), + submenu: [ + { + role: 'close', + label: i18n.__('close') + }, + { + role: 'minimize', + label: i18n.__('minimize') + }, + { + role: 'zoom', + label: i18n.__('zoom') + }, + { + type: 'separator' + }, + { + role: 'front', + label: i18n.__('front') + }, + { + type: 'separator' + }, + { + label: i18n.__('screenshot'), + accelerator: 'CmdOrCtrl+S', + async click(item, focusedWindow) { + if (focusedWindow) { + await focusedWindow.webContents.capturePage().then((image) => { + let screenshotFile = path.join(os.tmpdir(), 'screenshot.png') + + console.log('image captured', screenshotFile) + + let url = focusedWindow.webContents.getURL() + if (url.startsWith('file://')) { + let normalized = path.normalize(url).replace('.html', '.png') + screenshotFile = normalized.replace('file:', '') + let thumbnailFile = screenshotFile.replace('index.png', 'thumbnail.png') + if (url.endsWith('index.html')) { + thumbnailFile = screenshotFile.replace('index.png', 'thumbnail.png') + } else { + let folderName = path.dirname(screenshotFile) + let baseName = path.basename(screenshotFile) + thumbnailFile = path.join(folderName, 'thumbnails', baseName) + } + fs.writeFile(thumbnailFile, thumbnail(image), (err) => { + if (err) { + throw err + } else { + console.log(`Thumbnail written to ${thumbnailFile}`) + } + }) + } + fs.writeFile(screenshotFile, image.toPNG(), (err) => { + if (err) { + throw err + } else { + console.log(`Screenshot written to ${screenshotFile}`) + } + }) + }) + } + } + }, + { + type: 'separator' + } + ] + }, + { + role: 'help', + label: i18n.__('help'), + submenu: [ + { + label: i18n.__('iwm'), + click() { + shell.openExternal('https://www.iwm-tuebingen.de') + } + } + ] + } +] + +if (process.platform === 'darwin') { + const name = app.getName() + template.unshift({ + label: name, + submenu: [ + { + role: 'about', + label: i18n.__('about') + }, + { + type: 'separator' + }, + { + role: 'quit', + label: i18n.__('quit') + } + ] + }) +} + +const menu = Menu.buildFromTemplate(template) +Menu.setApplicationMenu(menu) + +checkBookmark(main.store.get('url')) +setHistoryStatus() + +function focus() { + findItem('id', 'forcereload').enabled = true + findItem('id', 'togglefullscreen').enabled = true + findItem('id', 'toggledevelopertools').enabled = true +} + +function blur() { + findItem('id', 'forcereload').enabled = false + findItem('id', 'togglefullscreen').enabled = false + findItem('id', 'toggledevelopertools').enabled = false +} + +module.exports = { + menu, + setHistoryStatus, + focus, + blur +} diff --git a/browser/package.js b/browser/package.js new file mode 100644 index 0000000..a6be579 --- /dev/null +++ b/browser/package.js @@ -0,0 +1,171 @@ +/* globals require, __dirname, process */ +/*eslint no-console: ["error", { allow: ["log", "info", "warn", "error"] }]*/ + +const fse = require('fs-extra') +const path = require('path') +const packager = require('electron-packager') +const rebuild = require('electron-rebuild') + +// Arguments +//---------------------- +let folder = null + +if (process.argv.length < 3) { + console.error('Missing command line parameter "folder"!') + process.exit(1) +} else { + folder = process.argv[2] +} + +// Settings +//---------------------- +let settings = null +const root = path.join(__dirname, '../') + +try { + settings = require(`../${folder}/settings.json`) +} catch (e) { + console.error('Cannot read settings.json in folder, does it exist?') + process.exit(1) +} + +// Read settings +//---------------------- +const title = `--- Build "${settings.name || settings.id}" ---` +const line = Array(title.length + 1).join('-') +console.info(line) +console.info(title) +console.info(line) + +// Using folder +//---------------------- +const tempFolder = path.join(root, 'temp', settings.id) +console.log(`Using folder ${tempFolder}`) + +// Delete temp folder (when last run aborted) +fse.removeSync(tempFolder) +console.log(`Folder ${tempFolder} deleted`) + +// Create folder +fse.ensureDirSync(tempFolder) +console.log(`Folder ${tempFolder} created`) + +// Create subfolders +const defaultFolders = ['assets', 'browser', 'css', 'lib', 'node_modules', 'server'] +console.log(`The folders ${defaultFolders.join(', ')} are included by default`) +const folders = new Set(settings.browser.folders.concat(defaultFolders)) +for (let folder of folders) { + console.log(`Copy folder ${folder}`) + const folderOld = path.join(root, folder) + const folderNew = path.join(root, 'temp', settings.id, folder) + + fse.copySync(folderOld, folderNew) +} + +// Write package.json +//---------------------- +let json = { + name: settings.id, + productName: settings.name || settings.id, + version: settings.version, + main: 'browser/main.js', + dependencies: {} +} + +// Read and write dependencies +const packageJson = fse.readJsonSync(path.join(root, 'package.json')) +Object.assign(json.dependencies, packageJson.dependencies) + +// Add browser dependencies +if (settings.browser.dependencies) { + let dependencies = {} + for (let dependency of settings.browser.dependencies) { + dependencies[dependency] = '*' + } + Object.assign(json.dependencies, dependencies) +} + +console.log('Create package.json') +fse.writeJsonSync(path.join(tempFolder, 'package.json'), json, {spaces: 4}) + +// Write URL to settings.json +//---------------------- +console.log('Write URL to browser/settings.json') +fse.writeJsonSync(path.join(tempFolder, 'browser/settings.json'), {url: `../${folder}/index.html`}, {spaces: 4}) + +// Build with electron-packager +//---------------------- +console.log('Start electron-packager') +packager({ + dir: `./temp/${settings.id}`, + arch: 'x64', + asar: false, + overwrite: true, + out: './dist/electron', + icon: './assets/icons/icon', + platform: settings.browser.platform || ['darwin', 'win32'], + prune: false, + afterCopy: [(buildPath, electronVersion, platform, arch, callback) => { + console.log(`Rebuild Node.js modules for ${platform}...`) + rebuild.rebuild({buildPath, electronVersion, arch}) + .then(() => { + console.log(`...Node.js modules for ${platform} rebuilded`) + callback() + }) + .catch(error => { + console.error(`Error: ${error}`) + callback(error) + }); + }] +}) + .then(appPaths => { + console.log('electron-packager finished') + + // Delete temp folder + //---------------------- + fse.removeSync(tempFolder) + console.log(`Folder ${tempFolder} deleted`) + + // Write data folders + //---------------------- + if (settings.browser.data) { + console.log('Copy data folders') + for (let folder of settings.browser.data) { + for (let appPath of appPaths) { + console.log(`Copy folder ${folder} to ${appPath}`) + const source = path.join(root, folder) + const target = path.join(getResourcesPath(root, appPath), folder) + fse.copySync(source, target, { + dereference: true, + filter: item => { + if (settings.browser.dataExtensions && fse.lstatSync(item).isFile() && !settings.browser.dataExtensions.includes(path.extname(item).substring(1).toLowerCase())) { + return false + } else { + return true + } + } + }) + } + } + } + + // Finished + //---------------------- + console.info('Finished') + }) + .catch(error => { + console.error(error) + }) + +function getResourcesPath(root, appPath) { + + let resourcesPath = "" + + if (/darwin/.test(appPath) || /mas/.test(appPath)) { + resourcesPath = path.join(root, appPath, `${json.productName}.app/Contents/Resources/app`) + } else if (/win32/.test(appPath) || /linux/.test(appPath)) { + resourcesPath = path.join(root, appPath, 'resources/app') + } + + return resourcesPath +} diff --git a/browser/pad.js b/browser/pad.js new file mode 100644 index 0000000..22cbd5a --- /dev/null +++ b/browser/pad.js @@ -0,0 +1,570 @@ +const {fileURL} = require('./utils.js') +const path = require('path') + +/* A specialization that ignores webview events and thus allows + * webviews to get touch, mouse and wheel events. + */ +class DOMPadContainer extends DOMScatterContainer { + + capture(event) { + if (event.target.tagName === 'WEBVIEW' || event.target.classList.contains('interactiveElement')) + return false + return super.capture(event) + } +} + +/* A wrapper for a webview that behaves like a virtual tablet browser. + * Uses a DOMScatter to zoom and rotate the virtual browser window. + * The position of buttons and the border size remain constant. + */ +class Pad { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + url="https://www.iwm-tuebingen.de/www/index.html", + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.x = x + this.y = y + this.url = url + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + this.frame.classList.add("pad") + this.border = 50 / startScale + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + display: 'flex', + width: this.width+"px", + height: this.height+"px", + top: 0, + left: 0, + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + overflow: "visible"}) + + document.body.appendChild( this.frame) + + this.web=document.createElement("webview") + this.webBackground=document.createElement("div") + this.webViewSnapshot=document.createElement("img") + this.overlay = document.createElement('div') + + this.loadAnim = document.createElement("div") + this.loadAnim.style.webkitAnimation= "spin 2s linear infinite" + this.loadAnim.style.animation= "spin 2s linear infinite" + this.loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + this.overlay.appendChild(this.loadAnim) + + Elements.setStyle(this.web, { + position: "absolute", + overflow: "auto", + border: "1px solid #fff" + }) + // this.web.classList.add("interactiveElement") + // this.web.style.pointerEvents="none" + + Elements.setStyle(this.webBackground, { + position: "absolute", + overflow: "auto", + background: "white" + }) + + Elements.setStyle(this.overlay, { + position: "absolute", + overflow: "auto", + background: "white", + opacity: "0.8" + }) + + Elements.setStyle(this.webViewSnapshot, { + position: "absolute", + overflow: "auto" + }) + + let timeTouchStart = 0 + this.overlayCaptureEvents = document.createElement('div') + // overlay.style.background="white" + + this.overlayCaptureEvents.classList.add("interactiveElement") + + this.overlayCaptureEvents.addEventListener('touchmove',(e)=>{ + e.preventDefault() + e.stopPropagation() + }) + + this.overlayCaptureEvents.addEventListener('pointerup',(e)=>{ + e.preventDefault() + e.stopPropagation() + let p = {x:e.clientX, y:e.clientY} + + let webviewPosition = Points.fromPageToNode(this.web,p) + + let d = new Date() + + if(d.getTime()-timeTouchStart<150)this.web.sendInputEvent({type:'mouseUp', x: webviewPosition.x, y: webviewPosition.y, button:'left', clickCount: 1}) + }) + + this.overlayCaptureEvents.addEventListener('pointerdown',(e)=>{ + e.preventDefault() + e.stopPropagation() + this.scatter.bringToFront() + let p = {x:e.clientX, y:e.clientY} + + let webviewPosition = Points.fromPageToNode(this.web,p) + + let d = new Date() + timeTouchStart = d.getTime() + + this.web.sendInputEvent({type:'mouseDown', x: webviewPosition.x, y: webviewPosition.y, button:'left', clickCount: 1}) + }) + + this.overlayCaptureEvents.addEventListener('pointermove',(e)=>{ + if(e.pointerType!='mouse'){ + let rotation = Angle.radian2degree(this.scatter.rotation); + rotation = (rotation + 360) % 360; + + let r = Math.sqrt(Math.pow(e.movementX, 2) + Math.pow(e.movementY, 2)); + let phi = Angle.radian2degree(Math.atan2(e.movementX, e.movementY)); + + phi = ((phi) + 630) % 360; + let rot = ((rotation + 90) + 630) % 360; + + let diffAngle = ((0 + rot) + 360) % 360; + let phiCorrected = (phi + diffAngle + 360) % 360; + + let deltaX = r * Math.cos(Angle.degree2radian(phiCorrected)); + let deltaY = -r * Math.sin(Angle.degree2radian(phiCorrected)); + + this.web.executeJavaScript("window.scrollTo(scrollX+"+(-1*deltaX)+", scrollY+"+ (-1*deltaY)+")") + + } + }) + + this.overlayCaptureEvents.addEventListener('mousewheel',(e)=>{ + console.log("mousewheel",e.deltaY) + // webview.sendInputEvent({type:'mouseWheel', x: 0, y: 0, deltaX: e.deltaX, deltaY: -e.deltaY, canScroll: true }) + this.web.executeJavaScript("window.scrollTo(scrollX+"+e.deltaX+", scrollY+"+ e.deltaY+")") + }) + + this.frame.appendChild(this.webBackground) + this.frame.appendChild(this.web) + this.frame.appendChild(this.webViewSnapshot) + this.frame.appendChild(this.overlay) + if(remote.getGlobal('multiUserMode'))this.frame.appendChild(this.overlayCaptureEvents) + + this.webViewSnapshot.style.visibility="hidden" + + this.web.src=url + this.web.preload= path.join(__dirname, './preloadPad.js') + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + this.backButton = this.addButton("../assets/icons/svg/left.svg", "go back") + this.forwardButton = this.addButton("../assets/icons/svg/right.svg", "go forward") + + this.backButton.style.opacity = 0.5 + this.forwardButton.style.opacity = 0.5 + + /*for (let callback of window.padLoadedHandler) { + callback(this, url) + }*/ + + this.web.addEventListener('new-window', (e) => { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new Pad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + }) + + this.web.addEventListener('did-navigate', (e) => { + this.enableButtons() + }) + + this.web.addEventListener('dom-ready',()=>{ + //if(this.url.indexOf('local')>-1)this.web.openDevTools() + }) + + this.web.addEventListener('ipc-message', (e) => { + if(e.channel='webviewPointerDown')this.scatter.bringToFront() + }) + + this.web.addEventListener('did-start-loading', ()=>{ + this.overlay.style.visibility="visible" + + let w = this.overlay.offsetWidth + let h = this.overlay.offsetHeight + + console.log("did start loading",h,w) + + let animationSize = w{ + this.overlay.style.visibility="hidden" + }) + + /*this.backButton.addEventListener('click', ()=>{ + if(this.web.canGoBack()) + this.web.goBack() + }) + + this.forwardButton.addEventListener('click', ()=>{ + if(this.web.canGoForward()) + this.web.goForward() + }) + + this.closeButton.addEventListener('click', ()=>{ + this.close() + })*/ + + InteractionMapper.on('tap',this.backButton, e => { + if(this.web.canGoBack()) + this.web.goBack() + }) + + InteractionMapper.on('tap',this.forwardButton, e => { + if(this.web.canGoForward()) + this.web.goForward() + }) + + InteractionMapper.on('tap',this.closeButton, e => { + this.close() + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + this.layout() + + } + + rad2degree(alpha){ + return alpha * 180 / Math.PI; + } + + degree2rad(alpha){ + return alpha * Math.PI / 180; + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + enableButtons() { + this.backButton.style.opacity = (this.web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (this.web.canGoForward()) ? 1 : 0.5 + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value="close" + button.draggable = false + button.classList.add("interactiveElement") + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + + this.scatter.resizeButton.style.width=b+"px" + this.scatter.resizeButton.style.height=b+"px" + + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + Elements.setStyle(this.web, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.webViewSnapshot, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.webBackground, { + width: size, + height: size, + margin: b+"px"}) + + Elements.setStyle(this.overlay, { + width: size, + height: size, + margin: b+"px"}) + + Elements.setStyle(this.overlayCaptureEvents, { + width: size, + height: size, + opacity: 0.0001, + background: "white", + margin: b+"px"}) + + Elements.setStyle(this.closeButton, { + right: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.backButton, { + left: (b * 0.8) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.forwardButton, { + left: (this.border + (b * 0.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + } + +} + +class PadFromElement { + + constructor(element, scatterContainer, { + startScale=1.0, minScale=0.1, maxScale=1.0, + autoBringToFront=true, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.element = element + + this.x = x + this.y = y + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + Elements.setStyle(this.frame, { + width: this.width+"px", + height: this.height+"px", + backgroundColor: "#333", + position: "fixed", + top: 0, + left: 0, + overflow: "auto"}) + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + + document.body.appendChild( this.frame) + this.border = 50 + + this.frame.appendChild(this.element) + + this.title = document.createElement("div") + this.title.innerHTML = "Titel" + this.title.style.color = "white" + this.frame.appendChild(this.title) + + Elements.setStyle(this.title, { + position: "absolute" + }) + // this.element.style.overflow = "auto" + // this.element.style.position = "absolute" + + this.layout() + + this.closeButton.addEventListener('click', ()=>{ + this.frame.style.display="none" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + this.scatter.bringToFront() + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.element.addEventListener('pointerdown', (e) => { + this.scatter.bringToFront() + }) + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value="close" + button.draggable = false + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + + this.scatter.resizeButton.style.width=b+"px" + this.scatter.resizeButton.style.height=b+"px" + + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + Elements.setStyle(this.element, { + width: size, + height: size, + top: b+"px", + left: b+"px"}) + Elements.setStyle(this.closeButton, { + right: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.title, { + left: (b * 1.5) + "px", + fontSize: (b * 0.8) + "px", + top: (0.1)+"0px"}) + } +} + +module.exports = { Pad, DOMPadContainer, PadFromElement } diff --git a/browser/padAccordion.js b/browser/padAccordion.js new file mode 100644 index 0000000..35e81df --- /dev/null +++ b/browser/padAccordion.js @@ -0,0 +1,3540 @@ +const {fileURL} = require('./utils.js') +const path = require('path') +const electron = require('electron'); +const BrowserWindow = electron.remote.BrowserWindow; + +/* A specialization that ignores webview events and thus allows + * webviews to get touch, mouse and wheel events. + */ +class DOMPadContainer extends DOMScatterContainer { + + capture(event) { + if (event.target.tagName === 'WEBVIEW' || event.target.classList.contains('interactiveElement')) + return false + return super.capture(event) + } +} + +/* A wrapper for a webview that behaves like a virtual tablet browser. + * Uses a DOMScatter to zoom and rotate the virtual browser window. + * The position of buttons and the border size remain constant. + */ +class Pad { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + title='new Pad', + type='pad', + url="https://www.iwm-tuebingen.de/www/index.html", + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.x = x + this.y = y + this.type = type + this.title=title + this.url = url + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + this.frame.classList.add("pad") + this.border = 50 / startScale + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + display: 'flex', + width: this.width+"px", + height: this.height+"px", + top: 0, + left: 0, + visibility: "visible", + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + overflow: "visible"}) + + document.body.appendChild( this.frame) + + this.web=document.createElement("webview") + this.webBackground=document.createElement("div") + this.webViewSnapshot=document.createElement("img") + this.overlay = document.createElement('div') + + this.loadAnim = document.createElement("div") + this.loadAnim.style.webkitAnimation= "spin 2s linear infinite" + this.loadAnim.style.animation= "spin 2s linear infinite" + this.loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + this.overlay.appendChild(this.loadAnim) + + Elements.setStyle(this.web, { + position: "absolute", + overflow: "auto", + border: "1px solid #fff" + }) + + Elements.setStyle(this.webBackground, { + position: "absolute", + overflow: "auto", + background: "white" + }) + + Elements.setStyle(this.overlay, { + position: "absolute", + overflow: "auto", + background: "white", + opacity: "0.8" + }) + + Elements.setStyle(this.webViewSnapshot, { + position: "absolute", + overflow: "auto" + }) + + this.frame.appendChild(this.webBackground) + this.frame.appendChild(this.web) + this.frame.appendChild(this.webViewSnapshot) + // this.frame.appendChild(this.overlay) + + this.webViewSnapshot.style.visibility="hidden" + + this.web.src=url + this.web.preload= path.join(__dirname, './preloadPad.js') + + this.closeButton = this.addButton("../dev/tumortisch/icons/svg/cross.svg", "close") + this.backButton = this.addButton("../dev/tumortisch/icons/svg/left.svg", "go back") + this.forwardButton = this.addButton("../dev/tumortisch/icons/svg/right.svg", "go forward") + + this.backButton.style.opacity = 0.5 + this.forwardButton.style.opacity = 0.5 + + /*for (let callback of window.padLoadedHandler) { + callback(this, url) + }*/ + this.layout() + + this.web.addEventListener('new-window', (e) => { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new Pad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + }) + + this.web.addEventListener('did-navigate', (e) => { + this.enableButtons() + }) + + this.web.addEventListener('dom-ready',()=>{ + //if(this.url.indexOf('local')>-1)this.web.openDevTools() + }) + + this.web.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + this.web.addEventListener('did-start-loading', ()=>{ + this.overlay.style.visibility="visible" + + let w = this.overlay.offsetWidth + let h = this.overlay.offsetHeight + + // console.log("did start loading",h,w) + + let animationSize = w{ + this.overlay.style.visibility="hidden" + let query="#"+this.title+"expandButton" + $(query).css("opacity","1.0") + }) + + this.backButton.addEventListener('click', (e)=>{ + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "./icons/resize.png" + this.scatter.resizeButton.appendChild(img) + // this.scatter.resizeButton.className = "frameButton" + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + } + + rad2degree(alpha){ + return alpha * 180 / Math.PI; + } + + degree2rad(alpha){ + return alpha * Math.PI / 180; + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + enableButtons() { + this.backButton.style.opacity = (this.web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (this.web.canGoForward()) ? 1 : 0.5 + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + button.classList.add("interactiveElement") + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + Elements.setStyle(this.web, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.webViewSnapshot, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.webBackground, { + width: size, + height: size, + margin: b+"px"}) + + Elements.setStyle(this.overlay, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.closeButton, { + right: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.backButton, { + left: (b * 0.8) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.forwardButton, { + left: (this.border + (b * 0.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + } + +} +class PadAccordion { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + type = 'accordion', + title="new Pad", + urlList=["https://www.iwm-tuebingen.de/www/index.html"], + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + minimizedHiddenHeight = 0, + minimizedHiddenWidth = 0, + minimized = false, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + let w = window, + d = document, + e = d.documentElement, + g = d.getElementsByTagName('body')[0] + this.pageWidth = w.innerWidth || e.clientWidth || g.clientWidth, + this.pageHeight = w.innerHeight|| e.clientHeight|| g.clientHeight + + this.x = x + this.y = y + this.type = type + this.title=title + this.urlList = urlList + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + this.minimizedHiddenHeight = minimizedHiddenHeight + this.minimizedHiddenWidth = minimizedHiddenWidth + this.minimized = minimized + + this.webviewID = 0 + this.externalPadID = 0 + this.currentWebviewBackground = null + this.currentWebviewFrame = null + this.currentWebview = null + this.currentWebviewImage = null + this.currentSource="" + this.webviewMap = new Map() + this.padMap = new Map() + this.frame = document.createElement('div') + this.webFrame = document.createElement('div') + this.titlebar = document.createElement('div') + this.frame.classList.add("pad") + this.border = this.pageHeight*0.023148 / startScale + + this.zIndex=1000 + this.numWebviewsReady = 0 + + this.closeAllButton = this.addButton("../dev/tumortisch/icons/clear_all.png", "closeAll") + this.minimizeButton = this.addButton("../dev/tumortisch/icons/remove.png", "minimize") + this.maximizeButton = this.addButton("../dev/tumortisch/icons/fullscreen.png", "maximize") + this.closeButton = this.addButton("../dev/tumortisch/icons/clear.png", "closeSingle") + this.backButton = this.addButton("../dev/tumortisch/icons/left.svg", "go back") + this.forwardButton = this.addButton("../dev/tumortisch/icons/right.svg", "go forward") + this.expandButton = this.addButton("../dev/tumortisch/icons/launch.png", "expand") + + this.expandButton.id=this.title+"expandButton" + + this.minimized == false ? this.minimizeButton.src = fileURL("../dev/tumortisch/icons/remove.png") : this.minimizeButton.src = fileURL("../dev/tumortisch/icons/fullscreen.png") + + this.backButton.style.opacity = 0.5 + this.forwardButton.style.opacity = 0.5 + TweenMax.to(this.maximizeButton, 0.0, {scale:"0"}) + + /*let pad = new Pad(nodePadContainer, { + x: 500, + y: 200, + title: this.title, + url: "var/blank.html", + startScale:1, + hideOnStart: false, + width: 1500, + height: 1500, + scalable: true, + rotatable: false}) + + + this.bufferPad=pad*/ + + // this.bufferPad.frame.id = this.title+"bufferPadFrame" + // this.bufferPad.web.id = this.title+"bufferPadWeb" + // this.bufferPad.frame.style.visibility="hidden" + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + width: this.width+"px", + height: this.height+"px", + visibility: "visible", + top: 0, + left: 0 + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + }) + + Elements.setStyle(this.webFrame, { + backgroundColor: "#333", + position: "absolute", + // display: 'flex', + // flexFlow: "row no-wrap", + overflowX: "hidden", + width: "100%", + height: "80%", + top: 0, + left: 0 + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + }) + + this.titlebar.innerHTML = this.title + Elements.setStyle(this.titlebar, { + color: "#fff", + // backgroundColor: "#F53", + // innerHTML:"title", + position: "absolute", + // display: 'flex', + // flexFlow: "row no-wrap", + overflowX: "hidden" + }) + + this.clicker=document.createElement('div') + + Elements.setStyle(this.clicker, { + backgroundColor: "#F53", + position: "absolute", + width: "60px", + height: "50px", + bottom: 0, + left: 0 + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + }) + + + document.body.appendChild( this.frame) + this.frame.appendChild( this.webFrame) + this.frame.appendChild( this.titlebar) + // this.frame.appendChild( this.clicker) + + this.clicker.addEventListener('click',(e)=>{ + this.arrangeWebviews() + }) + + for(var i=0; i { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new Pad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + }) + + this.web.addEventListener('did-navigate', (e) => { + this.enableButtons() + }) + + this.web.addEventListener('dom-ready',()=>{ + //if(this.url.indexOf('local')>-1)this.web.openDevTools() + }) + + this.web.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + this.web.addEventListener('did-start-loading', ()=>{ + this.overlay.style.visibility="visible" + + let w = this.overlay.offsetWidth + let h = this.overlay.offsetHeight + + console.log("did start loading",h,w) + + let animationSize = w{ + this.overlay.style.visibility="hidden" + })*/ + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "./icons/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.currentWebviewHeight = 0 + + this.scatter.element.addEventListener('resized',(e)=>{ + // $(this.webFrame).find('.accordionWebimage').css('height',this.currentWebviewHeight) + + this.arrangeWebviews() + // console.log("resized",e,e.detail) + }) + + this.scatter.element.addEventListener('resizeStarted',()=>{ + // this.currentWebviewHeight = this.webFrame.offsetHeight + // console.log("resizeStarted") + }) + + this.scatter.element.addEventListener('resizeEnded',()=>{ + /*let h = this.webFrame.offsetHeight + let cWebview = this.currentWebview + $(this.webFrame).find('.accordionWebview').each(function(i, obj) { + // obj.style.visibility="visible" + if(obj!=cWebview){ + obj.style.visibility="visible" + obj.capturePage((image)=>{ + obj.parentElement.childNodes[3].src=image.toDataURL() + + obj.style.visibility="hidden" + $(this.webFrame).find('.accordionWebimage').css('height',h) + }) + + } + })*/ + // console.log("resizeEnded") + }) + + this.scatter.addTransformEventCallback((e) => { + let newBorder = this.pageHeight*0.023148 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + else{ + } + }) + this.layout() + + InteractionMapper.on('tap',this.backButton, e => { + if(this.currentWebview.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.currentWebview.goBack() + } + }) + + InteractionMapper.on('tap',this.forwardButton, e => { + if(this.currentWebview.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.currentWebview.goForward() + } + }) + + InteractionMapper.on('tap',this.closeAllButton, e => { + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + InteractionMapper.on('tap',this.minimizeButton, e => { + let s = this.scatter.scale + let x=0 + let y=0 + let deltaX = 0 + let deltaY = 0 + switch(this.minimized){ + case false: + + let w = this.frame.offsetWidth*s < 800 ? this.frame.offsetWidth : 800/s + + x = this.frame.getBoundingClientRect().right - w + y = this.frame.getBoundingClientRect().bottom - this.border + + deltaX = this.frame.offsetWidth - w + deltaY = this.frame.offsetHeight - this.border + + + this.minimizedHiddenHeight = this.frame.offsetHeight + this.minimizedHiddenWidth = this.frame.offsetWidth + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + + this.minimize(w, this.border, this.scatter.scale, deltaX*s, deltaY*s) + this.minimized = true + this.minimizeButton.src = fileURL("../dev/tumortisch/icons/fullscreen.png") + this.scatter.scalable = false + // this.frame.style.pointerEvents = "none" + // this.minimizeButton.style.pointerEvents = "all" + // this.closeAllButton.style.pointerEvents = "all" + $(this.frame).children(".frameButton").each(function(i, obj) { + if(obj.value !="close" && obj.value !="closeAll" && obj.value !="minimize")obj.style.pointerEvents = "none" + }) + break + case true: + + s = this.scatter.scale + + x = this.frame.getBoundingClientRect().right - this.minimizedHiddenWidth + y = this.frame.getBoundingClientRect().bottom - this.minimizedHiddenHeight + + deltaX = this.frame.getBoundingClientRect().width - this.minimizedHiddenWidth*s + deltaY = this.frame.getBoundingClientRect().height - this.minimizedHiddenHeight*s + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + + this.minimize(this.minimizedHiddenWidth, this.minimizedHiddenHeight, this.scatter.scale, deltaX, deltaY) + this.minimized = false + this.minimizeButton.src = fileURL("../dev/tumortisch/icons/remove.png") + this.arrangeWebviews() + this.scatter.scalable = true + $(this.frame).children(".frameButton").each(function(i, obj) { + obj.style.pointerEvents = "all" + }) + // this.frame.style.pointerEvents = "all" + break + } + }) + + /* InteractionMapper.on('tap',this.minimizeButton, e => { + + let p = {x:e.center.x, y:e.center.y} + + // let framePosition = Points.fromPageToNode(this.frame,p) + + let s = this.scatter.scale + + let w = this.frame.offsetWidth*s < 800 ? this.frame.offsetWidth : 800/s + + // let x = this.frame.getBoundingClientRect().x + this.frame.offsetWidth*s - w + // let y = this.frame.getBoundingClientRect().y + this.frame.offsetHeight*s - this.border + + let x = this.frame.getBoundingClientRect().right - w + let y = this.frame.getBoundingClientRect().bottom - this.border + + let deltaX = this.frame.offsetWidth - w + let deltaY = this.frame.offsetHeight - this.border + + let tmp = this.frame.getBoundingClientRect().bottom - this.frame.getBoundingClientRect().top + + console.log("minimizeButton", s, this.frame.getBoundingClientRect().x, this.frame.offsetHeight*s, this.frame.offsetWidth*s, tmp) + + this.currentHeight = this.frame.offsetHeight + this.currentWidth = this.frame.offsetWidth + this.currentWebHeight = this.webFrame.offsetHeight + this.currentScale = s + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + // TweenMax.to(this.webFrame, 1.1, {height:"0px"}) + // TweenMax.to(this.frame, 0.2, {height:this.border, width:w}) + this.resize(w, this.border, this.scatter.scale, deltaX*s, deltaY*s) + + TweenMax.to(this.minimizeButton, 0.0, {scale:"0"}) + TweenMax.to(this.maximizeButton, 0.0, {scale:"1"}) + + }) + + InteractionMapper.on('tap',this.maximizeButton, e => { + console.log("maximizeButton", this.frame.getBoundingClientRect().width, this.currentWidth, this.frame.getBoundingClientRect().width - this.currentWidth) + let p = {x:e.center.x, y:e.center.y} + + // let framePosition = Points.fromPageToNode(this.frame,p) + + let s = this.scatter.scale + + // let r = this.frame.offsetWidth - framePosition.x + // let b = this.frame.offsetHeight - framePosition.y + + // let x = this.frame.getBoundingClientRect().x + this.frame.offsetWidth - this.currentWidth + // let y = this.frame.getBoundingClientRect().y + this.frame.offsetHeight - this.currentHeight + + let x = this.frame.getBoundingClientRect().right - this.currentWidth + let y = this.frame.getBoundingClientRect().bottom - this.currentHeight + + let deltaX = this.frame.getBoundingClientRect().width - this.currentWidth*s + let deltaY = this.frame.getBoundingClientRect().height - this.currentHeight*s + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + // TweenMax.to(this.webFrame, 0.1, {height:this.currentWebHeight+"px"}) + this.resize(this.currentWidth, this.currentHeight, this.scatter.scale, deltaX, deltaY) + // TweenMax.to(this.frame, 0.2, {height: this.currentHeight+"px", width: this.currentWidth+"px", x: x, y: y}) + + TweenMax.to(this.minimizeButton, 0.0, {scale:"1"}) + TweenMax.to(this.maximizeButton, 0.0, {scale:"0"}) + }) */ + + InteractionMapper.on('tap',this.closeButton, e => { + if(this.webviewMap.size>1)this.removeWebView() + }) + + InteractionMapper.on('tap',this.expandButton, e => { + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.externalPadID++ + let newPadKey=this.title+this.externalPadID + console.log("NEW PAD KEY",newPadKey) + console.log("NEW PAD ACCORDION",this.currentWebview.src) + + let newPad= new minimalPad(nodePadContainer, { + x: 500, + y: 200, + padMapKey: newPadKey, + title: this.title+" - "+ this.expandButton.getAttribute('source'),//this.currentWebview.getTitle(), + urlList: [this.currentWebview.src], + startScale:1, + hideOnStart: false, + width: this.pageWidth*0.3906, + height: this.pageWidth*0.3906, + scalable: true, + rotatable: false}) + + urlPadMap.set(newPadKey,newPad) + }) + + /*this.backButton.addEventListener('click', (e)=>{ + if(this.currentWebview.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.currentWebview.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.currentWebview.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.currentWebview.goForward() + } + }) + + this.closeAllButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + this.closeButton.addEventListener('click', (e)=>{ + if(this.webviewMap.size>1)this.removeWebView() + }) + + this.expandButton.addEventListener('click',(e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.externalPadID++ + let newPadKey=this.title+this.externalPadID + console.log("NEW PAD KEY",newPadKey) + console.log("NEW PAD ACCORDION",this.currentWebview.src) + // console.log("expanding",this.currentWebview.getTitle()) + // console.log("expanding",this.currentWebview.getURL()) + + let newPad= new minimalPad(nodePadContainer, { + x: 500, + y: 200, + padMapKey: newPadKey, + title: this.title+" - "+ this.expandButton.getAttribute('source'),//this.currentWebview.getTitle(), + urlList: [this.currentWebview.src], + startScale:1, + hideOnStart: false, + width: this.pageWidth*0.3906, + height: this.pageWidth*0.3906, + scalable: true, + rotatable: false}) + + /* let newPad= new PadAccordion(nodePadContainer, { + x: 500, + y: 200, + padMapKey: newPadKey, + title: this.title+" - "+ this.expandButton.getAttribute('source'),//this.currentWebview.getTitle(), + urlList: [{'url':this.currentWebview.src,'source':this.expandButton.getAttribute('source')}], + startScale:1, + hideOnStart: false, + width: this.pageWidth*0.3906, + height: this.pageWidth*0.3906, + scalable: true, + rotatable: false}) */ + + // urlPadMap.set(newPadKey,newPad) + /* document.getElementById(this.title+"bufferPadFrame").style.visibility="visible" + document.getElementById(this.title+"bufferPadFrame").id="" + document.getElementById(this.title+"bufferPadWeb").id=""*/ + /*if(e.target.style.opacity=="1"){ + this.bufferPad.frame.style.visibility="visible" + this.bufferPad.scatter.bringToFront() + // this.bufferPad.frame.id="" + // this.bufferPad.web.id="" + + let newPad= new Pad(nodePadContainer, { + x: 500, + y: 200, + title: this.title, + url: "var/blank.html", + startScale:1, + hideOnStart: false, + width: 1500, + height: 1500, + scalable: true, + rotatable: false}) + + this.bufferPad=newPad + // newPad.web.src=this.currentWebview.src + // this.bufferPad.frame.id = this.title+"bufferPadFrame" + // this.bufferPad.web.id = this.title+"bufferPadWeb" + this.bufferPad.frame.style.visibility = "hidden" + this.bufferPad.web.src=this.currentWebview.src + }*/ + // }) + // TweenMax.to(this.frame,0,{opacity:"0.001"}) + + } + + minimize(width, height, scale, x, y) { + + let oldPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + + this.frame.style.transformOrigin = "0% 0%" + + let newPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + + let offset = Points.subtract(oldPostition, newPostition) + + TweenLite.to(this.frame, 0, { css: { left: "+=" + offset.x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + offset.y + "px" } }) + + // TweenLite.to(this.frame, 0, { width: width , height: height, x: x, y: y }) + TweenLite.to(this.frame, 0, { width: width , height: height}) + TweenLite.to(this.frame, 0, { css: { left: "+=" + x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + y + "px" } }) + + oldPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + this.frame.style.transformOrigin = "50% 50%" + newPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + offset = Points.subtract(oldPostition, newPostition) + + TweenLite.to(this.frame, 0, { css: { left: "+=" + offset.x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + offset.y + "px" } }) + + } + + removeWebView(){ + let webVIewRemovedID=parseInt(this.currentWebview.getAttribute('id')) + + this.webviewMap.delete(webVIewRemovedID) + this.webFrame.removeChild(this.currentWebview.parentNode) + + let cWeb = null + let cWebviewFrame = null + + this.webviewMap.forEach(function (entry, key) { + cWeb = entry.childNodes[3] + cWebviewFrame = entry + }) + this.currentWebview = cWeb + this.currentWebviewFrame = cWebviewFrame + this.currentSource = cWeb.src + this.bringWebToFront() + } + + appendWebView(newUrl,source){ + let fs = nodeRequire("fs") + let timeTouchStart = 0 + + let web=document.createElement("webview") + let webOffloading=document.createElement("webview") + let webImage=document.createElement("img") + let dummyImage=document.createElement("img") + let heinz=document.createElement("img") + let webBackground=document.createElement("div") + let webviewFrame=document.createElement("div") + let shadow=document.createElement("img") + let shadowReverse=document.createElement("img") + let overlay = document.createElement('img') + let overlayCaptureEvents = document.createElement('div') + let progressBar = document.createElement('div') + + web.setAttribute("plugins",true) + web.classList.add("accordionWebview") + // web.classList.add("interactiveElement") + webviewFrame.classList.add("accordionWebviewFrame") + shadow.classList.add("accordionWebviewShadow") + shadowReverse.classList.add("accordionWebviewShadow") + webBackground.classList.add("accordionWebviewBackground") + webBackground.setAttribute('source',source) + webImage.classList.add("accordionWebimage") + overlay.classList.add("accordionOverlay") + overlay.src="img/overlay.png" + + overlayCaptureEvents.classList.add("interactiveElement") + + this.expandButton.setAttribute('source',source) + + webviewFrame.classList.add("interactiveElement") + webBackground.classList.add("interactiveElement") + webImage.classList.add("interactiveElement") + overlay.classList.add("interactiveElement") + web.classList.add("interactiveElement") + + let loadAnim = document.createElement("div") + loadAnim.style.webkitAnimation= "spin 2s linear infinite" + loadAnim.style.animation= "spin 2s linear infinite" + loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + // overlay.appendChild(loadAnim) + + // webBackground.className= "item" + + Elements.setStyle(webviewFrame, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: this.webviewID*50+"px", + // display:"flex", + width: "20%", + // pointerEvents: 'none', + left: "0%", + // minWidth:"5px", + // maxWidth:"500px", + height: "100%" + // boxShadow: "-5px 5px 50px 10px #555", + // border:"2px solid blue", + // overflow: "hidden", + // overflowX: "scroll", + // flex: "0 0 50%", + // whiteSpace: "nowrap", + }) + + Elements.setStyle(webBackground, { + position: "absolute", + overflow: "hidden", + left: "5%", + // display:"flex", + width: "90%", + pointerEvents: 'none', + // minWidth:"5px", + // maxWidth:"500px", + height: "100%", + // boxShadow: "-5px 5px 50px 10px #555", + // border:"2px solid blue", + // overflow: "hidden", + // overflowX: "scroll", + // flex: "0 0 50%", + // whiteSpace: "nowrap", + background: "white" + }) + + shadow.src = "img/shadow.png" + Elements.setStyle(shadow, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "95%", + width: "5%", + height: "100%", + // background: "#252", + // opacity: "0.1", + pointerEvents:"none" + }) + + shadowReverse.src = "img/shadowReverse.png" + Elements.setStyle(shadowReverse, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "0%", + width: "5%", + height: "100%", + // background: "#252", + // opacity: "0.1", + pointerEvents:"none" + }) + + Elements.setStyle(web, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + width: "90%", + // pointerEvents: 'none', + // filter: "blur(10px)", + // display: "none", + visibility: "hidden", + // border: "1px solid #fff", + height: "100%" + }) + + Elements.setStyle(webOffloading, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "-100%", + width: "90%", + // pointerEvents: 'none', + // filter: "blur(10px)", + // display: "none", + visibility: "visible", + // border: "1px solid #fff", + height: "100%" + }) + + Elements.setStyle(webImage, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + width: "90%", + pointerEvents: 'none', + // display: "none", + visibility: "visible", + // border: "1px solid #fff", + height: this.webFrame.offsetHeight+"px" + }) + + Elements.setStyle(dummyImage, { + visibility: "visible" + }) + + Elements.setStyle(heinz, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "0px", + width: "100%", + background: "pink", + // display: "none", + visibility: "visible", + // border: "1px solid #fff", + height: "100%" + }) + + Elements.setStyle(overlay, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + // background: "pink", + // pointerEvents: 'none', + width: "90%", + height: "100%" + // opacity: "0.1" + }) + + Elements.setStyle(overlayCaptureEvents, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + background: "white", + // pointerEvents: 'none', + width: "90%", + height: "100%", + opacity: "0.0001" + }) + + Elements.setStyle(progressBar, { + position: "absolute", + overflow: "hidden", + top: "0%", + left: "10%", + visibility: "hidden", + background: "white", + filter: "blur(50px)", + opacity: "0.2", + // pointerEvents: 'none', + width: "40%", + height: "100%" + // opacity: "0.1" + }) + + + this.currentWebviewFrame=webviewFrame + this.currentWebviewBackground=webBackground + this.currentWebview = web + this.currentWebviewImage = webImage + this.currentSource = source + + // document.body.appendChild(webOffloading) + + overlayCaptureEvents.addEventListener('touchmove',(e)=>{ + e.preventDefault() + e.stopPropagation() + }) + + overlayCaptureEvents.addEventListener('pointerup',(e)=>{ + e.preventDefault() + e.stopPropagation() + + let d = new Date() + console.log("delta Time",d.getTime()-timeTouchStart) + if(d.getTime()-timeTouchStart<150)web.sendInputEvent({type:'mouseUp', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale, button:'left', clickCount: 1}) + }) + + overlayCaptureEvents.addEventListener('pointerdown',(e)=>{ + web.send('overlayEvent') + e.preventDefault() + e.stopPropagation() + + let d = new Date() + timeTouchStart = d.getTime() + console.log("timeTouchStart",timeTouchStart) + web.sendInputEvent({type:'mouseDown', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale, button:'left', clickCount: 1}) + }) + + overlayCaptureEvents.addEventListener('pointermove',(e)=>{ + if(e.pointerType!='mouse'){ + // console.log("e.movementY",e.movementY) + web.executeJavaScript("window.scrollTo(scrollX+"+(-1*e.movementX)+", scrollY+"+ (-1*e.movementY)+")") + } + // if(e.pointerType=='mouse'){ + // web.sendInputEvent({type:'mouseMove', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale}) + // } + }) + + overlayCaptureEvents.addEventListener('mousewheel',(e)=>{ + // console.log("mousewheel",e.deltaY) + web.executeJavaScript("window.scrollTo(scrollX+"+e.deltaX+", scrollY+"+ e.deltaY+")") + // web.sendInputEvent({type:'mouseWheel', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale, deltaY: -e.deltaY}) + }) + + this.webFrame.appendChild(webviewFrame) + webviewFrame.appendChild(webBackground) + webviewFrame.appendChild(shadow) + webviewFrame.appendChild(shadowReverse) + webviewFrame.appendChild(web) + webviewFrame.appendChild(webImage) + // webBackground.appendChild(heinz) + if(remote.getGlobal('multiUserMode'))webviewFrame.appendChild(overlayCaptureEvents) + webviewFrame.appendChild(overlay) + webviewFrame.appendChild(progressBar) + + web.setAttribute('id',this.webviewID) + web.setAttribute('isLoading','true') + this.webviewMap.set(this.webviewID, webviewFrame) + this.webviewID++ + + shadow.addEventListener('pointermove',(e)=>{ + console.log("pointer moving over shadow") + }) + + let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/PreviewSnapshots/phantom_'+source+"_"+this.title+'.png') + dummyImage.src=captureFile + // web.src=captureFile + // webImage.src=captureFile + web.src=newUrl + // console.log("newUrl",newUrl) + // webOffloading.src=newUrl + /*let zoomFactor = 1 + + document.body.appendChild(dummyImage) + + dummyImage.addEventListener("load", ()=>{ + let w = (dummyImage.width/dummyImage.height) * this.webFrame.offsetHeight * 2 + + zoomFactor=this.webFrame.offsetWidth/w + console.log("IMAGE LOADEED",newUrl, w,this.webFrame.offsetWidth) + });*/ + + // web.src="https://www.golem.de/" + + // console.log("Appending WebView",newUrl) + // // document.getElementById(this.title+"bufferPadWeb").src=newUrl + web.preload= path.join(__dirname, './preloadPad.js') + + web.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + web.addEventListener('did-fail-load',()=>{ + console.log("failed loading",newUrl) + }) + let obj = { blur: 2 } + let loadAnimation = new TimelineMax({repeat:-1}) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 5,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 2,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"50%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0, {left:"20%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"50%"}) ) + loadAnimation.stop() + // loadAnimation.add( TweenLite.to(web, 1, {filter:"blur(0px)"}) ); + + web.addEventListener('did-start-loading', ()=>{ + web.setAttribute('isLoading','true') + loadAnimation.play() + overlay.style.visibility="visible" + progressBar.style.visibility="visible" + web.style.filter = "blur(10px)" + overlay.style.pointerEvents="none" + + // web.style.filter = "blur(10px)" + + // console.log('did-start-loading') + // loadAnim.style.visibility="visible" + + let w = overlay.offsetWidth + let h = overlay.offsetHeight + + // console.log("accordion webview did start loading",h,w) + + /*let animationSize = w{ + // web.setZoomFactor(1.0) + + /*let win = new BrowserWindow({width: 800, height: 600, x: -1000, y: -1000, webPreferences: {offscreen: true}}) + win.webContents.on('paint', (event, dirty, image) => { + webImage.src=image.toDataURL() + win.close() + }) + win.loadURL(newUrl)*/ + + if(this.currentWebview==web)overlay.style.visibility="hidden" + overlay.style.pointerEvents="all" + // loadAnimation.stop() + loadAnimation.stop() + progressBar.style.visibility="hidden" + web.style.filter = "blur(0px)" + web.setAttribute('isLoading','false') + // console.log('did-stop-loading') + // loadAnim.style.visibility="hidden" + if(web==this.currentWebview){ + // this.bufferPad.web.src=web.src + } + if(web!=this.currentWebview){ + } + // if(this.webviewID{ + // webImage.src=image.toDataURL() + })*/ + + // console.log('did-stop-loading',webImage) + }) + /*web.addEventListener('did-navigate', ()=>{ + console.log('did-navigate') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('did-navigate-in-page', ()=>{ + console.log('did-navigate-in-page') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('did-frame-finish-load', ()=>{ + console.log('did-frame-finish-load') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('load-commit', ()=>{ + console.log('load-commit') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('page-title-updated', ()=>{ + console.log('page-title-updated') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + })*/ + + + /*let phantom = require('phantom') + + let date = new Date() + + let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/PreviewSnapshots/phantom_'+source+"_"+this.title+'.png') + if (fs.existsSync(captureFile)) { + fs.unlink(captureFile, (err) => { + if (err) { + return + } + console.log("File succesfully deleted") + }) + } + phantom.create().then(function(ph) { + ph.createPage().then(function(page) { + page.open(web.src).then(function(status) { + page.render(captureFile).then(function(){ + webImage.src=captureFile + }) + }) + }) + })*/ + web.addEventListener('dom-ready', ()=>{ + + /*let id=this.webviewID + let nextURL = this.urlList[id].url + let nextSource = this.urlList[id].source*/ + + // webImage.src='var/'+source+'.png' + // console.log('dom-ready') + web.setAttribute('isLoading','false') + if(web==this.currentWebview){ + // this.bufferPad.web.src=web.src + } + if(web!=this.currentWebview){ + } + /* this.currentWebviewBackground = web.parentElement + this.currentWebview = web + this.currentSource = source + this.arrangeWebviews()*/ + + let captureRect = { + x: 0, y: 0, + width: 2000, + height: 4000 + } + web.capturePage(captureRect,function(image) { + webImage.src=image.toDataURL() + // console.log("web.capturePage",newUrl) + let buf = image.toPNG() + let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/'+source+'.png') + + fs.writeFile(captureFile, buf, function(err) { + if (err) { + console.log("ERROR Failed to save file", err) + } + else{ + } + }) + }) + /*this.numWebviewsReady++ + if(this.numWebviewsReady==7){ + this.arrangeWebviews() + TweenMax.to(this.frame,0.0,{opacity:"1"}) + }*/ + + /* web.capturePage((image)=>{ + // webImage.src='var/cosmic.png' + // console.log("Captured after Load",web.src) + webImage.src=image.toDataURL() + + /* let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/'+source+'.png') + let buf = image.toPng() + + fs.writeFile(captureFile, buf, function(err) { + if (err) { + console.log("ERROR Failed to save file", err) + } + else{ + } + })*/ + //}) + this.backButton.style.opacity = (web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (web.canGoForward()) ? 1 : 0.5 + // webImage.style.display="inline" + // if(source.indexOf("sum") >-1)web.openDevTools() + }) + + web.addEventListener('new-window', (e) => { + this.appendWebView(e.url) + // console.log("this.currentSource",source) + this.urlList.push({'url':e.url,'source':source}) + }) + + overlay.addEventListener('click',(e)=>{ + // console.log("Overlay wurde geklickt",this.currentWebview.src) + e.stopPropagation() + e.preventDefault() + this.currentWebview.capturePage((image)=>{ + // console.log(this.webFrame.offsetHeight) + this.currentWebviewImage.style.height = this.webFrame.offsetHeight+"px"//"100%" + this.currentWebviewImage.src=image.toDataURL() + this.currentWebviewFrame = e.target.parentElement + // this.bringWebToFront() + e.target.style.visibility="hidden" + + this.currentSource=source + this.expandButton.setAttribute('source',source) + + this.currentWebviewFrame = e.target.parentElement + this.currentWebview = web + this.currentSource = source + this.currentWebviewImage = webImage + this.arrangeWebviews() + // this.bufferPad.web.src=web.src + + this.backButton.style.opacity = (web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (web.canGoForward()) ? 1 : 0.5 + }) + //e.target.parentElement.style.zIndex=this.zIndex + + }) + /*overlay.addEventListener('click',(e)=>{ + e.stopPropagation() + e.preventDefault() + this.currentWebviewBackground = e.target.parentElement + this.bringWebToFront() + e.target.style.visibility="hidden" + //e.target.parentElement.style.zIndex=this.zIndex + })*/ + + webImage.addEventListener('pointerenter',(e)=>{ + // TweenMax.to(webBackground, 0.1, {x:"+= 50"}) + }) + + webImage.addEventListener('pointerleave',(e)=>{ + // TweenMax.to(webBackground, 0.1, {x:"-= 50"}) + }) + + webImage.addEventListener('click',(e)=>{ + + this.currentSource=source + this.expandButton.setAttribute('source',source) + + // this.expandButton.style.opacity="0.5" + e.stopPropagation() + e.preventDefault() + let oldWeb=this.currentWebview + let oldWebImage = this.currentWebviewImage + + oldWeb.capturePage((image)=>{ + oldWebImage.src=image.toDataURL() + + this.currentWebviewBackground = e.target.parentElement + this.currentWebview = web + this.currentWebviewImage = webImage + this.currentSource = source + this.arrangeWebviews() + }) + + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + + /* let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/'+source+'.png') + let buf = image.toPNG() + + fs.writeFile(captureFile, buf, function(err) { + if (err) { + console.log("ERROR Failed to save file", err) + } + else{ + } + })*/ + }) + + // this.bufferPad.web.src=web.src + + this.backButton.style.opacity = (web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (web.canGoForward()) ? 1 : 0.5 + // e.target.style.visibility="hidden" + //e.target.parentElement.style.zIndex=this.zIndex + }) + + /*overlay.addEventListener('pointerenter',(e)=>{ + TweenMax.to(webBackground, 0.1, {x:"+= 20"}) + }) + + overlay.addEventListener('pointerleave',(e)=>{ + TweenMax.to(webBackground, 0.1, {x:"-= 20"}) + })*/ + this.arrangeWebviews() + } + + arrangeWebviews(){ + let numWebviews = this.webviewMap.size + let index = 0 + let increment = 1 + let z = this.zIndex + let current=this.currentWebviewFrame + let shadowWidth = 0 + this.webviewMap.forEach(function (entry, key) { + entry.style.maxWidth="1000px" + // entry.style.pointerEvents = "none" + // entry.style.width=100-Math.sqrt(entry.parentElement.offsetWidth-590)+"%" + entry.style.width="70%" + shadowWidth = $(entry).find(".accordionWebviewShadow").outerWidth() + // console.log("entry.parentElement.offsetWidth",entry.parentElement.offsetWidth) + z+=increment + let offset=index*(entry.parentElement.offsetWidth+2*shadowWidth-entry.offsetWidth)/(numWebviews-1) + // console.log("Offset",entry.parentElement.offsetWidth-(offset+entry.offsetWidth)) + entry.style.left=entry.parentElement.offsetWidth+2*shadowWidth-(offset+entry.offsetWidth)-shadowWidth+"px" + entry.style.zIndex=z + $( entry ).children(".accordionWebview[isLoading='false']").css( "visibility", "hidden" ) + // $( entry ).children(".accordionWebview").css( "visibility", "hidden" ) + /* $( entry ).children(".accordionWebview").css( "visibility", "hidden" ).each(function(i, obj) { + console.log("isLoading()",obj.isLoading()) + })*/ + $( entry ).children('.accordionOverlay').css( "visibility", "visible" ) + $( entry ).children('.accordionWebimage').css( "visibility", "visible" ) + // entry.style.left=index*(entry.parentElement.offsetWidth-entry.offsetWidth)/(numWebviews-1)+"px" + index++ + if(entry==current){ + $( entry ).children('.accordionWebview').css( "visibility", "visible" ) + $( entry ).children('.accordionOverlay').css( "visibility", "hidden" ) + $( entry ).children('.accordionWebimage').css( "visibility", "hidden" ) + increment*=-1 + // entry.childNodes[1].style.opacity=0.8 + } + }) + this.zIndex=z + // this.currentWebviewFrame.style.pointerEvents = "all" + + // this.enableButtons() + /*this.zIndex++ + this.currentWebviewBackground.style.zIndex=this.zIndex + $( this.currentWebviewBackground ).children('.accordionWebview').css( "visibility", "visible" ) + $( this.currentWebviewBackground ).children('.accordionOverlay').css( "visibility", "hidden" ) + $( this.currentWebviewBackground ).children('.accordionWebimage').css( "visibility", "hidden" )*/ +} + + bringWebToFront(){ + this.arrangeWebviews() + this.zIndex++ + this.currentWebviewFrame.style.zIndex=this.zIndex + // this.currentWebviewBackground.childNodes[2].style.opacity=0.8 + } + rad2degree(alpha){ + return alpha * 180 / Math.PI; + } + + degree2rad(alpha){ + return alpha * Math.PI / 180; + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + urlPadMap.delete(this.title) + let geneTab = document.getElementById(this.title +"_tab") + TweenMax.to(geneTab, 0.4, {scaleX:"1.0",x:"0px"}); + geneTab.style.boxShadow = "none" + geneTab.setAttribute('open','false') + } + + enableButtons() { + this.backButton.style.opacity = (this.currentWebview.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (this.currentWebview.canGoForward()) ? 1 : 0.5 + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + button.classList.add("interactiveElement") + this.frame.appendChild(button) + return button + } + + layout() { + // console.log("LAYOUT") + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + let h = "calc(100% - " + (b) +"px)" + let w = "calc(100% - "+2 +"px)" + + this.scatter.resizeButton.style.width=b+"px" + this.scatter.resizeButton.style.height=b+"px" + + Elements.setStyle(this.webFrame, { + width: w, + height: h, + margin: "1px"}) + /*Elements.setStyle(this.webBackground, { + width: size, + height: size, + margin: b+"px"}) + + Elements.setStyle(this.overlay, { + width: size, + height: size, + margin: b+"px"})*/ + + Elements.setStyle(this.titlebar, { + left: (b * 5.75) + "px", + bottom: "0px", + fontSize: 0.7*b+"px", + // paddingTop: 10+"px", + // textAlign: "center", + margin: 0.1*b+"px", + height: 0.8*b+"px"}) + Elements.setStyle(this.closeAllButton, { + right: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.minimizeButton, { + right: (b * 3.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.maximizeButton, { + right: (b * 3.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.backButton, { + left: (b * 0.8) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.forwardButton, { + left: (this.border + (b * 0.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.expandButton, { + left: (this.border + (b * 1.8)) +"px", + bottom: "0px", + // opacity: "0.5", + margin: 0.1*b+"px", + width: 0.8*b+"px", + height: 0.8*b+"px"}) + Elements.setStyle(this.closeButton, { + left: (this.border + (b * 2.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + } + +} + +class PadFromElement { + + constructor(element, scatterContainer, { + startScale=1.0, minScale=0.1, maxScale=1.0, + autoBringToFront=true, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + type = 'fromElement', + title = 'New Pad', + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.element = element + this.type = type + this.title = title + this.x = x + this.y = y + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + Elements.setStyle(this.frame, { + width: this.width+"px", + height: this.height+"px", + backgroundColor: "#333", + position: "fixed", + visibility: "visible", + top: 0, + left: 0, + overflow: "auto"}) + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + + document.body.appendChild( this.frame) + this.border = 50 + + this.frame.appendChild(this.element) + + this.title = document.createElement("div") + this.title.innerHTML = "Titel" + this.title.style.color = "white" + this.frame.appendChild(this.title) + + Elements.setStyle(this.title, { + position: "absolute" + }) + // this.element.style.overflow = "auto" + // this.element.style.position = "absolute" + + this.layout() + + /*this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.frame.style.visibility="hidden" + })*/ + + InteractionMapper.on('tap',this.closeButton, e => { + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.frame.style.visibility="hidden" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + this.scatter.bringToFront() + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.element.addEventListener('pointerdown', (e) => { + this.scatter.bringToFront() + }) + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + // urlPadMap.delete(this.url) + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + button.classList.add("interactiveElement") + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + Elements.setStyle(this.element, { + width: size, + height: size, + top: b+"px", + left: b+"px"}) + Elements.setStyle(this.closeButton, { + right: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.title, { + left: (b * 1.5) + "px", + fontSize: (b * 0.8) + "px", + top: (0.1)+"0px"}) + } +} +class minimalPad { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + type = 'minimal', + title="new Pad", + urlList=["https://www.iwm-tuebingen.de/www/index.html"], + padMapKey="key", + // url="https://www.iwm-tuebingen.de/www/index.html", + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + minimizedHiddenHeight = 0, + minimizedHiddenWidth = 0, + minimized = false, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + let w = window, + d = document, + e = d.documentElement, + g = d.getElementsByTagName('body')[0] + this.pageWidth = w.innerWidth || e.clientWidth || g.clientWidth, + this.pageHeight = w.innerHeight|| e.clientHeight|| g.clientHeight + + this.x = x + this.y = y + this.type = type + // this.url = url + this.urlList = urlList + this.padMapKey = padMapKey, + this.title = title, + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + this.minimizedHiddenHeight = minimizedHiddenHeight + this.minimizedHiddenWidth = minimizedHiddenWidth + this.minimized = minimized + + this.web = null + this.WebviewCounter = 0 + this.id=0 + + this.webviewMap = new Map() + this.overlayMap = new Map() + this.tabMap = new Map() + this.listMap = new Map() + + this.frame = document.createElement('div') + this.frame.classList.add("pad") + this.border = this.pageHeight*0.023148 / startScale + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + display: 'flex', + width: this.width+"px", + height: this.height+"px", + visibility: "visible", + top: 0, + left: 0, + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + overflow: "visible"}) + + document.body.appendChild( this.frame) + + this.tabs=document.createElement("div") + this.webBackground=document.createElement("div") + this.webviewList=document.createElement("div") + this.titlebar = document.createElement("div") + this.webViewSnapshot=document.createElement("img") + + this.tabs.classList.add("interactiveElement") + this.tabs.classList.add("tabs") + // this.tabs.style.display="flex" + // this.tabs.style.flexFlow="row nowrap" + + Elements.setStyle(this.tabs, { + position: "absolute", + background: "white", + overflowX: "auto", + overflowY: "hidden", + display: "flex", + flexFlow:"row nowrap", + // justifyContent: "flex-end", + alignItems: "flex-end" + // overflow: "auto", + }) + + this.titlebar.innerHTML = this.title + Elements.setStyle(this.titlebar, { + color: "#fff", + position: "absolute", + overflowX: "hidden" + }) + + Elements.setStyle(this.webBackground, { + position: "absolute", + overflow: "hidden", + background: "white" + }) + + this.webviewList.classList.add("interactiveElement") + + Elements.setStyle(this.webviewList, { + position: "absolute", + visibility: "hidden", + overflow: "auto", + background: "white", + boxShadow: "5px 5px 10px #bbb" + }) + + /*Elements.setStyle(this.overlay, { + position: "absolute", + overflow: "auto", + background: "white", + opacity: "0.8" + })*/ + + Elements.setStyle(this.webViewSnapshot, { + position: "absolute", + overflow: "auto" + }) + + this.frame.appendChild(this.titlebar) + this.frame.appendChild(this.webBackground) + // this.frame.appendChild(this.webViewSnapshot) + this.frame.appendChild(this.tabs) + this.frame.appendChild(this.webviewList) + this.showWebviewListButton = this.addButton("../dev/tumortisch/icons/ic_list_48px.svg", "show list") + this.expandButton = this.addButton("../dev/tumortisch/icons/launch.png", "expand") + this.expandButton.id=this.title+"expandButton" + + this.showWebviewListButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.webviewList.style.visibility=="visible" ? this.webviewList.style.visibility="hidden" : this.webviewList.style.visibility="visible" + }) + + this.expandButton.addEventListener('click',(e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.externalPadID++ + let newPadKey=this.title+this.externalPadID + // console.log("NEW PAD KEY",newPadKey) + // console.log("expanding",this.currentWebview.getTitle()) + // console.log("expanding",this.currentWebview.getURL()) + + let newPad= new minimalPad(nodePadContainer, { + x: 500, + y: 200, + padMapKey: newPadKey, + title: this.title+" - "+ this.expandButton.getAttribute('source'),//this.currentWebview.getTitle(), + urlList: [e.target.getAttribute('url')], + startScale:1, + hideOnStart: false, + width: this.pageWidth*0.3906, + height: this.pageWidth*0.3906, + scalable: true, + rotatable: false}) + + urlPadMap.set(newPadKey,newPad) + }) + + for(var i=0; i{ + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameTop.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameLeft.addEventListener('pointerenter',(e)=>{ + // e.target.setPointerCapture(e.pointerId) + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameLeft.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameRight.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameRight.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + /*this.web.addEventListener('new-window', (e) => { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new minimalPad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + })*/ + InteractionMapper.on('tap',this.minimizeButton, e => { + let s = this.scatter.scale + let x=0 + let y=0 + let deltaX = 0 + let deltaY = 0 + switch(this.minimized){ + case false: + + let w = this.frame.offsetWidth*s < 800 ? this.frame.offsetWidth : 800/s + + x = this.frame.getBoundingClientRect().right - w + y = this.frame.getBoundingClientRect().bottom - this.border + + deltaX = this.frame.offsetWidth - w + deltaY = this.frame.offsetHeight - this.border + + this.minimizedHiddenHeight = this.frame.offsetHeight + this.minimizedHiddenWidth = this.frame.offsetWidth + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + + this.minimize(w, this.border, this.scatter.scale, deltaX*s, deltaY*s) + this.minimized = true + this.minimizeButton.src = fileURL("../dev/tumortisch/icons/fullscreen.png") + this.scatter.scalable = false + $(this.frame).children(".frameButton").each(function(i, obj) { + if(obj.value !="close" && obj.value !="closeAll" && obj.value !="minimize")obj.style.pointerEvents = "none" + }) + // this.frame.style.pointerEvents = "none" + // this.minimizeButton.style.pointerEvents = "all" + // this.closeButton.style.pointerEvents = "all" + break + case true: + + s = this.scatter.scale + + x = this.frame.getBoundingClientRect().right - this.minimizedHiddenWidth + y = this.frame.getBoundingClientRect().bottom - this.minimizedHiddenHeight + + deltaX = this.frame.getBoundingClientRect().width - this.minimizedHiddenWidth*s + deltaY = this.frame.getBoundingClientRect().height - this.minimizedHiddenHeight*s + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + + this.minimize(this.minimizedHiddenWidth, this.minimizedHiddenHeight, this.scatter.scale, deltaX, deltaY) + this.minimized = false + this.minimizeButton.src = fileURL("../dev/tumortisch/icons/remove.png") + this.scatter.scalable = true + $(this.frame).children(".frameButton").each(function(i, obj) { + obj.style.pointerEvents = "all" + }) + // this.frame.style.pointerEvents = "all" + break + } + }) + + InteractionMapper.on('tap',this.backButton, e => { + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + InteractionMapper.on('tap',this.forwardButton, e => { + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + InteractionMapper.on('tap',this.closeButton, e => { + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + /* this.backButton.addEventListener('click', (e)=>{ + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + })*/ + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "./icons/resize.png" + this.scatter.resizeButton.appendChild(img) + // this.scatter.resizeButton.className = "frameButton" + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.scatter.addTransformEventCallback((e) => { + let newBorder = this.pageHeight*0.023148 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.layout() + + /*this.test = document.createElement('div') + + Elements.setStyle(this.test, { + position: "absolute", + width: "400px", + height: "400px", + overflow: "auto", + background: "pink" + }) + this.frame.appendChild(this.test) + + this.test.addEventListener('click',()=>{ + // this.frame.style.visibility="hidden" + console.log($( this.webBackground ).children()) + $( this.webBackground ).children().css( "visibility", "hidden" ) + $( this.frame ).children().css( "visibility", "hidden" ) + })*/ + + } + + minimize(width, height, scale, x, y) { + + let oldPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + + this.frame.style.transformOrigin = "0% 0%" + + let newPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + + let offset = Points.subtract(oldPostition, newPostition) + + TweenLite.to(this.frame, 0, { css: { left: "+=" + offset.x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + offset.y + "px" } }) + + // TweenLite.to(this.frame, 0, { width: width , height: height, x: x, y: y }) + TweenLite.to(this.frame, 0, { width: width , height: height}) + TweenLite.to(this.frame, 0, { css: { left: "+=" + x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + y + "px" } }) + + oldPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + this.frame.style.transformOrigin = "50% 50%" + newPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + offset = Points.subtract(oldPostition, newPostition) + + TweenLite.to(this.frame, 0, { css: { left: "+=" + offset.x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + offset.y + "px" } }) + + } + + addNewWebview(src){ + /* if(this.web!=null){ + this.web.style.visibility="hidden" + this.web.setAttribute('selected','0') + }*/ + + let webview=document.createElement("webview") + let progressBar = document.createElement('div') + webview.setAttribute("plugins",true) + this.web=webview + webview.setAttribute('loaded','0') + this.webBackground.appendChild(webview) + this.webviewMap.set(src+this.padMapKey, webview) + // console.log("number of webviews",this.webviewMap.size) + // $("webview").each(function(i, obj) { + // obj.setAttribute('selected','0') + // }) + + Elements.setStyle(webview, { + position: "absolute", + overflow: "auto", + width: "100%", + height: "100%", + // border: "1px solid #fff" + }) + + Elements.setStyle(progressBar, { + position: "absolute", + overflow: "hidden", + top: "0%", + left: "10%", + visibility: "hidden", + background: "white", + filter: "blur(50px)", + opacity: "0.2", + // pointerEvents: 'none', + width: "40%", + height: "100%" + // opacity: "0.1" + }) + + webview.src=src + webview.preload= path.join(__dirname, './preloadPad.js') + + let listItem=document.createElement('div') + listItem.className="interactiveElement" + listItem.classList.add("webviewListItem") + listItem.setAttribute('id',src+this.padMapKey) + // listItem.style.height = "5%" + // // listItem.style.width = "80%" + // listItem.style.marginLeft="10%" + // listItem.style.marginTop="5%" + listItem.style.padding = "10px" + // listItem.style.paddingBottom = "10px" + this.webviewList.appendChild(listItem) + this.listMap.set(src+this.padMapKey,listItem) + + webview.setAttribute('id',src+this.padMapKey) + + this.setSelectedWebview(src+this.padMapKey) + + this.addNewTab(webview,src) + + listItem.addEventListener('click',(e)=>{ + console.log(e.target.getAttribute('id')) + this.setSelectedWebview(e.target.getAttribute('id')) + this.webviewList.style.visibility = "hidden" + $(this.tabs).animate({scrollLeft: $(".tab[id='"+e.target.getAttribute('id')+"']").prop('offsetLeft') - 10}, "slow") + this.enableButtons() + }) + + listItem.addEventListener('pointerenter',(e)=>{ + e.target.style.background = "#ddd" + let webview=null + this.webviewMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + if(item.getAttribute('selected')=='0')item.style.opacity="0.5" + webview=item + // obj.style.width="50%" + // obj.style.height="50%" + } + }) + this.overlayMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + } + }) + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + }) + + listItem.addEventListener('pointerleave',(e)=>{ + e.target.style.background = "#fff" + if(e.target.getAttribute('selected')=='1') e.target.style.background = "#999" + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('selected')=='0'){ + item.style.visibility="hidden" + } + if(item.getAttribute('selected')=='1'){ + item.style.visibility="visible" + item.style.opacity="1" + } + // obj.style.width="100%" + // obj.style.height="100%" + }) + }) + + let overlay = document.createElement('img') + overlay.style.position="absolute" + overlay.style.width="100%" + overlay.style.height="100%" + overlay.style.top="0px" + overlay.style.left="0px" + // overlay.style.background="white" + overlay.classList.add("minimalPadOverlay") + overlay.src="img/overlay.png" + + this.overlayMap.set(src,overlay) + + let timeTouchStart = 0 + + let overlayCaptureEvents = document.createElement('div') + overlayCaptureEvents.style.position="absolute" + overlayCaptureEvents.style.background="white" + overlayCaptureEvents.style.opacity="0.01" + overlayCaptureEvents.style.width="100%" + overlayCaptureEvents.style.height="100%" + overlayCaptureEvents.style.top="0px" + overlayCaptureEvents.style.left="0px" + // overlay.style.background="white" + + overlayCaptureEvents.classList.add("interactiveElement") + + overlayCaptureEvents.addEventListener('touchmove',(e)=>{ + e.preventDefault() + e.stopPropagation() + }) + + overlayCaptureEvents.addEventListener('pointerup',(e)=>{ + e.preventDefault() + e.stopPropagation() + + let d = new Date() + console.log("delta Time",d.getTime()-timeTouchStart) + if(d.getTime()-timeTouchStart<150)web.sendInputEvent({type:'mouseUp', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale, button:'left', clickCount: 1}) + }) + + overlayCaptureEvents.addEventListener('pointerdown',(e)=>{ + e.preventDefault() + e.stopPropagation() + + let d = new Date() + timeTouchStart = d.getTime() + console.log("timeTouchStart",timeTouchStart) + web.sendInputEvent({type:'mouseDown', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale, button:'left', clickCount: 1}) + }) + + overlayCaptureEvents.addEventListener('pointermove',(e)=>{ + if(e.pointerType!='mouse'){ + // console.log("e.movementY",e.movementY) + web.executeJavaScript("window.scrollTo(scrollX+"+(-1*e.movementX)+", scrollY+"+ (-1*e.movementY)+")") + } + // if(e.pointerType=='mouse'){ + // web.sendInputEvent({type:'mouseMove', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale}) + // } + }) + + overlayCaptureEvents.addEventListener('mousewheel',(e)=>{ + // console.log("mousewheel",e.deltaY) + web.executeJavaScript("window.scrollTo(scrollX+"+e.deltaX+", scrollY+"+ e.deltaY+")") + // web.sendInputEvent({type:'mouseWheel', x: (e.clientX-web.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-web.getBoundingClientRect().top)/this.scatter.scale, deltaY: -e.deltaY}) + }) + + let loadAnim = document.createElement("div") + loadAnim.style.webkitAnimation= "spin 2s linear infinite" + loadAnim.style.animation= "spin 2s linear infinite" + loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule("div.tabs::-webkit-scrollbar {display: none;}"); + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + // overlay.appendChild(loadAnim) + + if(remote.getGlobal('multiUserMode'))this.webBackground.appendChild(overlayCaptureEvents) + this.webBackground.appendChild(overlay) + this.webBackground.appendChild(progressBar) + + webview.addEventListener('did-navigate', (e) => { + this.enableButtons() + //this.backButton.style.opacity = (webview.canGoBack()) ? 1 : 0.5 + //this.forwardButton.style.opacity = (webview.canGoForward()) ? 1 : 0.5 + }) + + webview.addEventListener('dom-ready',()=>{ + // console.log("DOM READY TABBED WEBVIEW!!!!!!!!!!") + listItem.innerHTML=webview.getTitle()+" "+src + $(".tab[id='"+src+this.padMapKey+"']").children()[0].innerHTML=webview.getTitle() + // $(".tab[id='"+src+"']").children()[0].innerHTML="READY LOADED" + $(this.tabs).animate({scrollLeft: $(".tab[id='"+src+"']").prop('offsetLeft') - 10}, "slow") + + }) + + webview.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + let obj = { blur: 2 } + let loadAnimation = new TimelineMax({repeat:-1}) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 5,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 2,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"60%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0, {left:"20%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"60%"}) ) + loadAnimation.stop() + + webview.addEventListener('did-start-loading', ()=>{ + console.log("started loading page") + + webview.setAttribute('loaded','0') + overlay.style.visibility="visible" + webview.style.filter = "blur(10px)" + progressBar.style.visibility="visible" + loadAnimation.play() + + let w = overlay.offsetWidth + let h = overlay.offsetHeight + + /*let animationSize = w{ + loadAnimation.stop() + webview.style.filter = "blur(0px)" + webview.setAttribute('loaded','1') + this.layout() + progressBar.style.visibility="hidden" + overlay.style.visibility="hidden" + }) + + webview.addEventListener('new-window', (e) => { + if (!this.webviewMap.has(e.url+this.padMapKey)){ + this.addNewWebview(e.url) + } + }) + this.id++ + } + + setSelectedWebview(id){ + let webview=null + this.webviewMap.forEach(function (item, key, mapObj) { + item.setAttribute('selected','0') + item.style.opacity = "0" + item.style.visibility = "hidden" + if(key==id){ + webview=item + item.setAttribute('selected','1') + item.style.opacity = "1" + item.style.visibility = "visible" + } + }) + + this.tabMap.forEach(function (tab, key, mapObj) { + tab.style.border="none" + if(key==id){ + tab.style.borderLeft="1px solid black" + tab.style.borderTop="1px solid black" + tab.style.borderRight="1px solid black" + } + }) + + this.listMap.forEach(function (entry, key, mapObj) { + entry.style.background = "white" + entry.setAttribute('selected','0') + if(key==id){ + entry.style.background = "#999" + entry.setAttribute('selected','1') + } + }) + this.web = webview + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + } + + addNewTab(webview,src){ + + this.tabMap.forEach(function (tab, key, mapObj) { + tab.style.border="none" + }) + let tab=document.createElement('div') + tab.className="tab" + tab.classList.add("interactiveElement") + tab.style.display="flex" + tab.style.flexFlow="row nowrap" + tab.style.textOverflow="ellipsis" + tab.setAttribute('id',src+this.padMapKey) + // tab.innerHTML=" " + tab.style.color="black" + tab.style.background="white" + tab.style.borderLeft="1px solid black" + tab.style.borderTop="1px solid black" + tab.style.borderRight="1px solid black" + tab.style.position="relative" + tab.style.borderRadius="5px 5px 0 0" + tab.style.maxWidth="25%" + tab.style.height="80%" + tab.style.alignItems= "center" + tab.setAttribute('source',src) + // tab.style.display="inline" + // tab.style.marginLeft="10px" + tab.style.paddingLeft="10px" + this.tabs.appendChild(tab) + this.tabMap.set(src+this.padMapKey,tab) + let newSelectedWebview=null + + let title = document.createElement('div') + title.classList.add('tabTitle') + // title.style.display="flex" + // title.style.flexFlow="row nowrap" + title.style.overflow="hidden" + title.style.whiteSpace="nowrap" + title.style.textOverflow="ellipsis" + title.style.pointerEvents="none" + title.innerHTML="new Tab" + this.expandButton.setAttribute('url',src) + tab.appendChild(title) + + let close = document.createElement('img') + close.setAttribute('id',src+this.padMapKey) + close.classList.add('tabCloseButton') + close.style.background="#666" + close.style.height="50%" + close.style.marginLeft="10px" + close.style.marginRight="10px" + close.style.borderRadius="100%" + close.style.visibility="hidden" + close.src = "../../assets/icons/svg/cross.svg" + tab.appendChild(close) + + close.addEventListener('click',(e)=>{ + e.stopPropagation() + e.preventDefault() + let newSelectedItem = false + + if(this.webviewMap.size>1){ + let next=this.getNextTab(e.target.getAttribute("id")) + let previous=this.getPreviousTab(e.target.getAttribute("id")) + + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('id')==e.target.getAttribute("id")){ + if(item.getAttribute('selected')=='1'){ + newSelectedItem=true + } + mapObj.delete(key) + $(item).remove() + } + }) + + this.tabMap.forEach(function (tab, key, mapObj) { + if(tab.getAttribute('id')==e.target.getAttribute("id")){ + mapObj.delete(key) + $(tab).remove() + } + }) + + this.listMap.forEach(function (entry, key, mapObj) { + if(entry.getAttribute('id')==e.target.getAttribute("id")){ + mapObj.delete(key) + } + }) + $(".webviewListItem[id='"+e.target.getAttribute("id")+"']").remove() + $(e.target).remove() + if(newSelectedItem){ + next!=null ? this.setSelectedWebview(next.getAttribute('id')) : this.setSelectedWebview(previous.getAttribute('id')) + } + } + }) + + tab.addEventListener('pointerenter',(e)=>{ + if(this.webviewMap.size>1)e.target.childNodes[1].style.visibility="visible" + let z=this.frame.style.zIndex + + let webview=null + e.target.style.background="#ddd" + this.webviewMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + if(item.getAttribute('selected')=='0')item.style.opacity="0.5" + webview=item + // obj.style.width="50%" + // obj.style.height="50%" + } + }) + this.overlayMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + } + }) + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + /*Popup.open({"text":webview.getTitle()}, + {x: e.clientX, y: e.clientY}, + { fontSize: "2vh", maxWidth: width*0.2, spacing: '10px', notchPosition: 'topLeft', zIndex: z+100})*/ + }) + + tab.addEventListener('pointerleave',(e)=>{ + e.target.childNodes[1].style.visibility="hidden" + e.target.style.background="#fff" + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('selected')=='0'){ + item.style.visibility="hidden" + } + if(item.getAttribute('selected')=='1'){ + item.style.visibility="visible" + item.style.opacity="1" + } + // obj.style.width="100%" + // obj.style.height="100%" + }) + //this.web.style.visibility="visible" + //this.web.style.opacity="1" + }) + tab.addEventListener('click',(e)=>{ + this.tabMap.forEach(function (item, key, mapObj) { + item.style.border="none" + }) + + e.target.style.borderLeft="1px solid black" + e.target.style.borderTop="1px solid black" + e.target.style.borderRight="1px solid black" + + this.setSelectedWebview(e.target.getAttribute('id')) + this.expandButton.setAttribute("url",e.target.getAttribute('source')) + + if(e.target.offsetLeft+e.target.offsetWidth>this.tabs.offsetWidth+this.tabs.scrollLeft){ + $(this.tabs).animate({scrollLeft: e.target.offsetLeft-this.tabs.offsetWidth+e.target.offsetWidth}, "slow") + } + if(e.target.offsetLeft 1){ + this.tabs.style.display = "flex" + this.showWebviewListButton.style.display = "inline" + + if(this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + top: b+"px", + width: w, + height: size-b, + margin: "1px"}) + Elements.setStyle(this.titlebar, { + display: "inline"}) + } + if(!this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + width: w, + height: size, + margin: "1px"}) + } + } + if(this.tabMap.size <= 1){ + this.tabs.style.display = "none" + this.showWebviewListButton.style.display = "none" + + if(this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + top: b+"px", + width: w, + height: size, + margin: "1px"}) + Elements.setStyle(this.titlebar, { + display: "inline"}) + } + if(!this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + width: w, + height: h, + margin: "1px"}) + } + } + + } + +} + +class minimalPadFromElement { + + constructor(element, scatterContainer, { + startScale=1.0, minScale=0.1, maxScale=1.0, + autoBringToFront=true, + type = 'minimalFromElement', + title = 'new Pad', + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + minimizedHiddenHeight = 0, + minimizedHiddenWidth = 0, + minimized = false, + resizable=false, + } ={}) { + + this.element = element + this.x = x + this.y = y + this.type = type + this.title = title + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + this.minimizedHiddenHeight = minimizedHiddenHeight + this.minimizedHiddenWidth = minimizedHiddenWidth + this.minimized = minimized + + this.frame = document.createElement('div') + this.toolButtonMap = new Map() + Elements.setStyle(this.frame, { + width: this.width+"px", + height: this.height+"px", + backgroundColor: "#333", + position: "fixed", + visibility: "visible", + top: 0, + left: 0, + overflow: "auto"}) + + + this.closeButton = this.addButton("../dev/tumortisch/icons/clear.png", "close") + this.minimizeButton = this.addButton("../dev/tumortisch/icons/remove.png", "minimize") + + this.minimized == false ? this.minimizeButton.src = fileURL("../dev/tumortisch/icons/remove.png") : this.minimizeButton.src = fileURL("../dev/tumortisch/icons/fullscreen.png") + // this.addContentButton = this.addButton("../assets/icons/png/flat/note_add.png", "addContent") + // this.copyButton = this.addButton("../assets/icons/png/flat/content_copy.png", "copy") + // this.pasteButton = this.addButton("../assets/icons/png/flat/content_paste.png", "paste") + // this.saveButton = this.addButton("../assets/icons/png/flat/save.png", "save") + // this.deleteButton = this.addButton("../assets/icons/png/flat/delete.png", "delete") + + document.body.appendChild( this.frame) + this.border = 50 + + this.frame.appendChild(this.element) + + // this.pseudoFrame = document.createElement('div') + this.pseudoFrameTop = document.createElement('div') + this.pseudoFrameLeft = document.createElement('div') + this.pseudoFrameRight = document.createElement('div') + + /*Elements.setStyle(this.pseudoFrame, { + position: "absolute", + top: "0px", + left: "0px", + width: "100%", + height: "100%", + pointerEvents: "none" + })*/ + + Elements.setStyle(this.pseudoFrameTop, { + position: "absolute", + // background: "pink", + top: "0px", + left: "0px", + width: "100%" + // height: "50px" + }) + + Elements.setStyle(this.pseudoFrameLeft, { + position: "absolute", + // background: "pink", + top: "0px", + left: "0px", + // width: "5px", + height: "100%" + }) + + Elements.setStyle(this.pseudoFrameRight, { + position: "absolute", + // background: "pink", + top: "0px", + right: "0px", + // width: "5px", + height: "100%" + }) + this.frame.appendChild(this.pseudoFrameTop) + this.frame.appendChild(this.pseudoFrameLeft) + this.frame.appendChild(this.pseudoFrameRight) + // this.frame.appendChild(this.pseudoFrame) + + this.pseudoFrameTop.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + // this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameTop.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameLeft.addEventListener('pointerenter',(e)=>{ + // e.target.setPointerCapture(e.pointerId) + this.scatter.scalable=false + // this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameLeft.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameRight.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + // this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameRight.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + // this.element.style.overflow = "auto" + // this.element.style.position = "absolute" + + + /*this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.frame.style.visibility="hidden" + })*/ + + InteractionMapper.on('tap',this.minimizeButton, e => { + let s = this.scatter.scale + let x=0 + let y=0 + let deltaX = 0 + let deltaY = 0 + switch(this.minimized){ + case false: + + let w = this.frame.offsetWidth*s < 800 ? this.frame.offsetWidth : 800/s + + x = this.frame.getBoundingClientRect().right - w + y = this.frame.getBoundingClientRect().bottom - this.border + + deltaX = this.frame.offsetWidth - w + deltaY = this.frame.offsetHeight - this.border + + this.minimizedHiddenHeight = this.frame.offsetHeight + this.minimizedHiddenWidth = this.frame.offsetWidth + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + + this.minimize(w, this.border, this.scatter.scale, deltaX*s, deltaY*s) + this.minimized = true + this.minimizeButton.src = fileURL("../dev/tumortisch/icons/fullscreen.png") + this.scatter.scalable = false + $(this.frame).children(".frameButton").each(function(i, obj) { + if(obj.value !="close" && obj.value !="closeAll" && obj.value !="minimize")obj.style.pointerEvents = "none" + }) + // this.frame.style.pointerEvents = "none" + // this.minimizeButton.style.pointerEvents = "all" + // this.closeButton.style.pointerEvents = "all" + break + case true: + + s = this.scatter.scale + + x = this.frame.getBoundingClientRect().right - this.minimizedHiddenWidth + y = this.frame.getBoundingClientRect().bottom - this.minimizedHiddenHeight + + deltaX = this.frame.getBoundingClientRect().width - this.minimizedHiddenWidth*s + deltaY = this.frame.getBoundingClientRect().height - this.minimizedHiddenHeight*s + + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + + this.minimize(this.minimizedHiddenWidth, this.minimizedHiddenHeight, this.scatter.scale, deltaX, deltaY) + this.minimized = false + this.minimizeButton.src = fileURL("../dev/tumortisch/icons/remove.png") + this.scatter.scalable = true + $(this.frame).children(".frameButton").each(function(i, obj) { + obj.style.pointerEvents = "all" + }) + // this.frame.style.pointerEvents = "all" + break + } + }) + + InteractionMapper.on('tap',this.closeButton, e => { + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.frame.style.visibility="hidden" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "./icons/resize.png" + this.scatter.resizeButton.appendChild(img) + // this.scatter.resizeButton.className = "frameButton" + + this.scatter.bringToFront() + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.element.addEventListener('pointerdown', (e) => { + this.scatter.bringToFront() + }) + this.layout() + } + + minimize(width, height, scale, x, y) { + + let oldPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + + this.frame.style.transformOrigin = "0% 0%" + + let newPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + + let offset = Points.subtract(oldPostition, newPostition) + + TweenLite.to(this.frame, 0, { css: { left: "+=" + offset.x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + offset.y + "px" } }) + + // TweenLite.to(this.frame, 0, { width: width , height: height, x: x, y: y }) + TweenLite.to(this.frame, 0, { width: width , height: height}) + TweenLite.to(this.frame, 0, { css: { left: "+=" + x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + y + "px" } }) + + oldPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + this.frame.style.transformOrigin = "50% 50%" + newPostition = { x: $(this.frame).position().left, y: $(this.frame).position().top } + offset = Points.subtract(oldPostition, newPostition) + + TweenLite.to(this.frame, 0, { css: { left: "+=" + offset.x + "px" } }) + TweenLite.to(this.frame, 0, { css: { top: "+=" + offset.y + "px" } }) + + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + button.classList.add("interactiveElement") + this.frame.appendChild(button) + return button + } + + addToolButton(src, value) { + console.log("adding tool button",value) + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + this.frame.appendChild(button) + this.toolButtonMap.set(value,button) + return button + } + + layout() { + console.log("layounting minmalPadFromElement") + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + + this.scatter.resizeButton.style.width=b+"px" + this.scatter.resizeButton.style.height=b+"px" + + let size = "calc(100% - " + (2*b) +"px)" + let h = "calc(100% - " + (b) +"px)" + let w = "calc(100% - "+2 +"px)" + let w2 = "calc(100% - " + (b+2) +"px)" + + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + + Elements.setStyle(this.element, { + width: w, + height: h, + top: 0+"px", + left: 0+"px"}) + + Elements.setStyle(this.pseudoFrameTop, { + width: w, + height: 0.1*b+"px", + margin: "0px"}) + Elements.setStyle(this.pseudoFrameLeft, { + width: 0.1*b+"px", + height: h, + margin: "0px"}) + Elements.setStyle(this.pseudoFrameRight, { + width: 0.1*b+"px", + height: h, + margin: "0px"}) + + Elements.setStyle(this.closeButton, { + right: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.minimizeButton, { + right: (b * 2.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + let buttonCounter=0 + this.toolButtonMap.forEach(function (entry, key) { + // console.log(entry,key) + Elements.setStyle(entry, { + left: (b * (buttonCounter+0.75)) + "px", + bottom: "0px", + margin: 0.1*b+"px", + width: 0.8*b+"px", + height: 0.8*b+"px"}) + + buttonCounter++ + } + ) + + /*Elements.setStyle(this.addContentButton, { + left: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.copyButton, { + left: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.pasteButton, { + left: (b * 2.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.saveButton, { + left: (b * 3.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.deleteButton, { + left: (b * 4.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"})*/ + } +} + +module.exports = { Pad, minimalPad, DOMPadContainer, PadFromElement, minimalPadFromElement, PadAccordion } diff --git a/browser/padAccordionOld.js b/browser/padAccordionOld.js new file mode 100644 index 0000000..785fb27 --- /dev/null +++ b/browser/padAccordionOld.js @@ -0,0 +1,2814 @@ +const {fileURL} = require('./utils.js') +const path = require('path') +const electron = require('electron'); + +/* A specialization that ignores webview events and thus allows + * webviews to get touch, mouse and wheel events. + */ +class DOMPadContainer extends DOMScatterContainer { + + capture(event) { + if (event.target.tagName === 'WEBVIEW' || event.target.classList.contains('interactiveElement')) + return false + return super.capture(event) + } +} + +/* A wrapper for a webview that behaves like a virtual tablet browser. + * Uses a DOMScatter to zoom and rotate the virtual browser window. + * The position of buttons and the border size remain constant. + */ +class Pad { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + title='new Pad', + type='pad', + url="https://www.iwm-tuebingen.de/www/index.html", + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.x = x + this.y = y + this.type = type + this.title=title + this.url = url + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + this.frame.classList.add("pad") + this.border = 50 / startScale + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + display: 'flex', + width: this.width+"px", + height: this.height+"px", + top: 0, + left: 0, + visibility: "visible", + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + overflow: "visible"}) + + document.body.appendChild( this.frame) + + this.web=document.createElement("webview") + this.webBackground=document.createElement("div") + this.webViewSnapshot=document.createElement("img") + this.overlay = document.createElement('div') + + this.loadAnim = document.createElement("div") + this.loadAnim.style.webkitAnimation= "spin 2s linear infinite" + this.loadAnim.style.animation= "spin 2s linear infinite" + this.loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + this.overlay.appendChild(this.loadAnim) + + Elements.setStyle(this.web, { + position: "absolute", + overflow: "auto", + border: "1px solid #fff" + }) + + Elements.setStyle(this.webBackground, { + position: "absolute", + overflow: "auto", + background: "white" + }) + + Elements.setStyle(this.overlay, { + position: "absolute", + overflow: "auto", + background: "white", + opacity: "0.8" + }) + + Elements.setStyle(this.webViewSnapshot, { + position: "absolute", + overflow: "auto" + }) + + this.frame.appendChild(this.webBackground) + this.frame.appendChild(this.web) + this.frame.appendChild(this.webViewSnapshot) + // this.frame.appendChild(this.overlay) + + this.webViewSnapshot.style.visibility="hidden" + + this.web.src=url + this.web.preload= path.join(__dirname, './preloadPad.js') + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + this.backButton = this.addButton("../assets/icons/svg/left.svg", "go back") + this.forwardButton = this.addButton("../assets/icons/svg/right.svg", "go forward") + + this.backButton.style.opacity = 0.5 + this.forwardButton.style.opacity = 0.5 + + /*for (let callback of window.padLoadedHandler) { + callback(this, url) + }*/ + this.layout() + + this.web.addEventListener('new-window', (e) => { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new Pad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + }) + + this.web.addEventListener('did-navigate', (e) => { + this.enableButtons() + }) + + this.web.addEventListener('dom-ready',()=>{ + //if(this.url.indexOf('local')>-1)this.web.openDevTools() + }) + + this.web.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + this.web.addEventListener('did-start-loading', ()=>{ + this.overlay.style.visibility="visible" + + let w = this.overlay.offsetWidth + let h = this.overlay.offsetHeight + + // console.log("did start loading",h,w) + + let animationSize = w{ + this.overlay.style.visibility="hidden" + let query="#"+this.title+"expandButton" + $(query).css("opacity","1.0") + }) + + this.backButton.addEventListener('click', (e)=>{ + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + } + + rad2degree(alpha){ + return alpha * 180 / Math.PI; + } + + degree2rad(alpha){ + return alpha * Math.PI / 180; + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + enableButtons() { + this.backButton.style.opacity = (this.web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (this.web.canGoForward()) ? 1 : 0.5 + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value="close" + button.draggable = false + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + Elements.setStyle(this.web, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.webViewSnapshot, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.webBackground, { + width: size, + height: size, + margin: b+"px"}) + + Elements.setStyle(this.overlay, { + width: size, + height: size, + margin: b+"px"}) + Elements.setStyle(this.closeButton, { + right: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.backButton, { + left: (b * 0.8) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.forwardButton, { + left: (this.border + (b * 0.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + } + +} +class PadAccordion { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + type = 'accordion', + title="new Pad", + urlList=["https://www.iwm-tuebingen.de/www/index.html"], + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + let w = window, + d = document, + e = d.documentElement, + g = d.getElementsByTagName('body')[0] + this.pageWidth = w.innerWidth || e.clientWidth || g.clientWidth, + this.pageHeight = w.innerHeight|| e.clientHeight|| g.clientHeight + + this.x = x + this.y = y + this.type = type + this.title=title + this.urlList = urlList + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.webviewID = 0 + this.externalPadID = 0 + this.currentWebviewBackground = null + this.currentWebviewFrame = null + this.currentWebview = null + this.currentWebviewImage = null + this.currentSource="" + this.webviewMap = new Map() + this.padMap = new Map() + this.frame = document.createElement('div') + this.webFrame = document.createElement('div') + this.titlebar = document.createElement('div') + this.frame.classList.add("pad") + this.border = this.pageHeight*0.023148 / startScale + + this.zIndex=1000 + this.numWebviewsReady = 0 + + this.closeAllButton = this.addButton("../assets/icons/png/flat/clear_all.png", "close all") + this.closeButton = this.addButton("../assets/icons/png/flat/clear.png", "close") + this.backButton = this.addButton("../assets/icons/svg/left.svg", "go back") + this.forwardButton = this.addButton("../assets/icons/svg/right.svg", "go forward") + this.expandButton = this.addButton("../assets/icons/png/flat/launch.png", "expand") + + this.expandButton.id=this.title+"expandButton" + + this.backButton.style.opacity = 0.5 + this.forwardButton.style.opacity = 0.5 + + /*let pad = new Pad(nodePadContainer, { + x: 500, + y: 200, + title: this.title, + url: "var/blank.html", + startScale:1, + hideOnStart: false, + width: 1500, + height: 1500, + scalable: true, + rotatable: false}) + + + this.bufferPad=pad*/ + + // this.bufferPad.frame.id = this.title+"bufferPadFrame" + // this.bufferPad.web.id = this.title+"bufferPadWeb" + // this.bufferPad.frame.style.visibility="hidden" + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + width: this.width+"px", + height: this.height+"px", + visibility: "visible", + top: 0, + left: 0 + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + }) + + Elements.setStyle(this.webFrame, { + backgroundColor: "#333", + position: "absolute", + // display: 'flex', + // flexFlow: "row no-wrap", + overflowX: "hidden", + width: "100%", + height: "80%", + top: 0, + left: 0 + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + }) + + this.titlebar.innerHTML = this.title + Elements.setStyle(this.titlebar, { + color: "#fff", + // backgroundColor: "#F53", + // innerHTML:"title", + position: "absolute", + // display: 'flex', + // flexFlow: "row no-wrap", + overflowX: "hidden" + }) + + this.clicker=document.createElement('div') + + Elements.setStyle(this.clicker, { + backgroundColor: "#F53", + position: "absolute", + width: "60px", + height: "50px", + bottom: 0, + left: 0 + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + }) + + + document.body.appendChild( this.frame) + this.frame.appendChild( this.webFrame) + this.frame.appendChild( this.titlebar) + // this.frame.appendChild( this.clicker) + + this.clicker.addEventListener('click',(e)=>{ + this.arrangeWebviews() + }) + + for(var i=0; i { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new Pad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + }) + + this.web.addEventListener('did-navigate', (e) => { + this.enableButtons() + }) + + this.web.addEventListener('dom-ready',()=>{ + //if(this.url.indexOf('local')>-1)this.web.openDevTools() + }) + + this.web.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + this.web.addEventListener('did-start-loading', ()=>{ + this.overlay.style.visibility="visible" + + let w = this.overlay.offsetWidth + let h = this.overlay.offsetHeight + + console.log("did start loading",h,w) + + let animationSize = w{ + this.overlay.style.visibility="hidden" + })*/ + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.currentWebviewHeight = 0 + + this.scatter.element.addEventListener('resized',(e)=>{ + // $(this.webFrame).find('.accordionWebimage').css('height',this.currentWebviewHeight) + + this.arrangeWebviews() + // console.log("resized",e,e.detail) + }) + + this.scatter.element.addEventListener('resizeStarted',()=>{ + // this.currentWebviewHeight = this.webFrame.offsetHeight + // console.log("resizeStarted") + }) + + this.scatter.element.addEventListener('resizeEnded',()=>{ + /*let h = this.webFrame.offsetHeight + let cWebview = this.currentWebview + $(this.webFrame).find('.accordionWebview').each(function(i, obj) { + // obj.style.visibility="visible" + if(obj!=cWebview){ + obj.style.visibility="visible" + obj.capturePage((image)=>{ + obj.parentElement.childNodes[3].src=image.toDataURL() + + obj.style.visibility="hidden" + $(this.webFrame).find('.accordionWebimage').css('height',h) + }) + + } + })*/ + // console.log("resizeEnded") + }) + + this.scatter.addTransformEventCallback((e) => { + let newBorder = this.pageHeight*0.023148 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + else{ + } + }) + this.layout() + + this.backButton.addEventListener('click', (e)=>{ + if(this.currentWebview.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.currentWebview.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.currentWebview.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.currentWebview.goForward() + } + }) + + this.closeAllButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + this.closeButton.addEventListener('click', (e)=>{ + if(this.webviewMap.size>1)this.removeWebView() + }) + + this.expandButton.addEventListener('click',(e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.externalPadID++ + let newPadKey=this.title+this.externalPadID + console.log("NEW PAD KEY",newPadKey) + // console.log("expanding",this.currentWebview.getTitle()) + // console.log("expanding",this.currentWebview.getURL()) + + let newPad= new minimalPad(nodePadContainer, { + x: 500, + y: 200, + padMapKey: newPadKey, + title: this.title+" - "+ this.expandButton.getAttribute('source'),//this.currentWebview.getTitle(), + urlList: [this.currentWebview.src], + startScale:1, + hideOnStart: false, + width: this.pageWidth*0.3906, + height: this.pageWidth*0.3906, + scalable: true, + rotatable: false}) + + urlPadMap.set(newPadKey,newPad) + /* document.getElementById(this.title+"bufferPadFrame").style.visibility="visible" + document.getElementById(this.title+"bufferPadFrame").id="" + document.getElementById(this.title+"bufferPadWeb").id=""*/ + /*if(e.target.style.opacity=="1"){ + this.bufferPad.frame.style.visibility="visible" + this.bufferPad.scatter.bringToFront() + // this.bufferPad.frame.id="" + // this.bufferPad.web.id="" + + let newPad= new Pad(nodePadContainer, { + x: 500, + y: 200, + title: this.title, + url: "var/blank.html", + startScale:1, + hideOnStart: false, + width: 1500, + height: 1500, + scalable: true, + rotatable: false}) + + this.bufferPad=newPad + // newPad.web.src=this.currentWebview.src + // this.bufferPad.frame.id = this.title+"bufferPadFrame" + // this.bufferPad.web.id = this.title+"bufferPadWeb" + this.bufferPad.frame.style.visibility = "hidden" + this.bufferPad.web.src=this.currentWebview.src + }*/ + }) + // TweenMax.to(this.frame,0,{opacity:"0.001"}) + + } + + removeWebView(){ + let webVIewRemovedID=parseInt(this.currentWebview.getAttribute('id')) + + this.webviewMap.delete(webVIewRemovedID) + this.webFrame.removeChild(this.currentWebview.parentNode) + + let cWeb = null + let cWebviewFrame = null + + this.webviewMap.forEach(function (entry, key) { + cWeb = entry.childNodes[3] + cWebviewFrame = entry + }) + this.currentWebview = cWeb + this.currentWebviewFrame = cWebviewFrame + this.currentSource = cWeb.src + this.bringWebToFront() + } + + appendWebView(newUrl,source){ + let fs = nodeRequire("fs") + + let web=document.createElement("webview") + let webOffloading=document.createElement("webview") + let webImage=document.createElement("img") + let heinz=document.createElement("img") + let webBackground=document.createElement("div") + let webviewFrame=document.createElement("div") + let shadow=document.createElement("img") + let shadowReverse=document.createElement("img") + let overlay = document.createElement('img') + let progressBar = document.createElement('div') + + web.setAttribute("plugins",true) + web.classList.add("accordionWebview") + // web.classList.add("interactiveElement") + webviewFrame.classList.add("accordionWebviewFrame") + shadow.classList.add("accordionWebviewShadow") + shadowReverse.classList.add("accordionWebviewShadow") + webBackground.classList.add("accordionWebviewBackground") + webBackground.setAttribute('source',source) + webImage.classList.add("accordionWebimage") + overlay.classList.add("accordionOverlay") + overlay.src="img/overlay.png" + + webviewFrame.classList.add("interactiveElement") + webBackground.classList.add("interactiveElement") + webImage.classList.add("interactiveElement") + overlay.classList.add("interactiveElement") + web.classList.add("interactiveElement") + + let loadAnim = document.createElement("div") + loadAnim.style.webkitAnimation= "spin 2s linear infinite" + loadAnim.style.animation= "spin 2s linear infinite" + loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + // overlay.appendChild(loadAnim) + + // webBackground.className= "item" + + Elements.setStyle(webviewFrame, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: this.webviewID*50+"px", + // display:"flex", + width: "20%", + left: "0%", + // minWidth:"5px", + // maxWidth:"500px", + height: "100%" + // pointerEvents: 'none', + // boxShadow: "-5px 5px 50px 10px #555", + // border:"2px solid blue", + // overflow: "hidden", + // overflowX: "scroll", + // flex: "0 0 50%", + // whiteSpace: "nowrap", + }) + + Elements.setStyle(webBackground, { + position: "absolute", + overflow: "hidden", + left: "5%", + // display:"flex", + width: "90%", + // pointerEvents: 'none', + // minWidth:"5px", + // maxWidth:"500px", + height: "100%", + // boxShadow: "-5px 5px 50px 10px #555", + // border:"2px solid blue", + // overflow: "hidden", + // overflowX: "scroll", + // flex: "0 0 50%", + // whiteSpace: "nowrap", + background: "white" + }) + + shadow.src = "img/shadow.png" + Elements.setStyle(shadow, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "95%", + width: "5%", + height: "100%", + // background: "#252", + // opacity: "0.1", + pointerEvents:"none" + }) + + shadowReverse.src = "img/shadowReverse.png" + Elements.setStyle(shadowReverse, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "0%", + width: "5%", + height: "100%", + // background: "#252", + // opacity: "0.1", + pointerEvents:"none" + }) + + Elements.setStyle(web, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + width: "90%", + // pointerEvents: 'none', + // filter: "blur(10px)", + // display: "none", + visibility: "hidden", + // border: "1px solid #fff", + height: "100%" + }) + + Elements.setStyle(webOffloading, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "-100%", + width: "90%", + // pointerEvents: 'none', + // filter: "blur(10px)", + // display: "none", + visibility: "visible", + // border: "1px solid #fff", + height: "100%" + }) + + Elements.setStyle(webImage, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + width: "90%", + height: this.webFrame.offsetHeight+"px", + pointerEvents: 'none', + // display: "none", + visibility: "visible" + // border: "1px solid #fff", + }) + + Elements.setStyle(heinz, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "0px", + width: "100%", + background: "pink", + // display: "none", + visibility: "visible", + // border: "1px solid #fff", + height: "100%" + }) + + Elements.setStyle(overlay, { + position: "absolute", + overflow: "hidden", + top: "0px", + left: "5%", + // background: "pink", + // pointerEvents: 'none', + width: "90%", + height: "100%" + // opacity: "0.1" + }) + + Elements.setStyle(progressBar, { + position: "absolute", + overflow: "hidden", + top: "0%", + left: "10%", + visibility: "hidden", + background: "white", + filter: "blur(50px)", + opacity: "0.2", + // pointerEvents: 'none', + width: "40%", + height: "100%" + // opacity: "0.1" + }) + + + this.currentWebviewFrame=webviewFrame + this.currentWebviewBackground=webBackground + this.currentWebview = web + this.currentWebviewImage = webImage + this.currentSource = source + + // document.body.appendChild(webOffloading) + + this.webFrame.appendChild(webviewFrame) + webviewFrame.appendChild(webBackground) + webviewFrame.appendChild(shadow) + webviewFrame.appendChild(shadowReverse) + webviewFrame.appendChild(web) + webviewFrame.appendChild(webImage) + // webBackground.appendChild(heinz) + webviewFrame.appendChild(overlay) + webviewFrame.appendChild(progressBar) + + web.setAttribute('id',this.webviewID) + web.setAttribute('isLoading','true') + this.webviewMap.set(this.webviewID, webviewFrame) + this.webviewID++ + web.src=newUrl + webOffloading.src=newUrl + + // web.src="https://www.golem.de/" + + // console.log("Appending WebView",newUrl) + // // document.getElementById(this.title+"bufferPadWeb").src=newUrl + // webImage.src='var/'+source+'.png' + web.preload= path.join(__dirname, './preloadPad.js') + + web.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + web.addEventListener('did-fail-load',()=>{ + console.log("failed loading",newUrl) + }) + let obj = { blur: 2 } + let loadAnimation = new TimelineMax({repeat:-1}) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 5,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 2,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"50%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0, {left:"20%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"50%"}) ) + loadAnimation.stop() + // loadAnimation.add( TweenLite.to(web, 1, {filter:"blur(0px)"}) ); + + web.addEventListener('did-start-loading', ()=>{ + web.setAttribute('isLoading','true') + loadAnimation.play() + overlay.style.visibility="visible" + progressBar.style.visibility="visible" + web.style.filter = "blur(10px)" + overlay.style.pointerEvents="none" + + // web.style.filter = "blur(10px)" + + // console.log('did-start-loading') + // loadAnim.style.visibility="visible" + + let w = overlay.offsetWidth + let h = overlay.offsetHeight + + // console.log("accordion webview did start loading",h,w) + + /*let animationSize = w{ + + /*let win = new BrowserWindow({width: 800, height: 600, x: -1000, y: -1000, webPreferences: {offscreen: true}}) + win.webContents.on('paint', (event, dirty, image) => { + webImage.src=image.toDataURL() + win.close() + }) + win.loadURL(newUrl)*/ + + if(this.currentWebview==web)overlay.style.visibility="hidden" + overlay.style.pointerEvents="all" + // loadAnimation.stop() + loadAnimation.stop() + progressBar.style.visibility="hidden" + web.style.filter = "blur(0px)" + web.setAttribute('isLoading','false') + // console.log('did-stop-loading') + // loadAnim.style.visibility="hidden" + if(web==this.currentWebview){ + // this.bufferPad.web.src=web.src + } + if(web!=this.currentWebview){ + } + // if(this.webviewID{ + // webImage.src=image.toDataURL() + })*/ + + // console.log('did-stop-loading',webImage) + }) + /*web.addEventListener('did-navigate', ()=>{ + console.log('did-navigate') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('did-navigate-in-page', ()=>{ + console.log('did-navigate-in-page') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('did-frame-finish-load', ()=>{ + console.log('did-frame-finish-load') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('load-commit', ()=>{ + console.log('load-commit') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + }) + web.addEventListener('page-title-updated', ()=>{ + console.log('page-title-updated') + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + }) + })*/ + + + /*let phantom = require('phantom') + + let date = new Date() + + let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/PhantomScreenshots/phantom_'+source+date.getMinutes()+date.getSeconds()+'.png') + if (fs.existsSync(captureFile)) { + fs.unlink(captureFile, (err) => { + if (err) { + return + } + console.log("File succesfully deleted") + }) + } + phantom.create().then(function(ph) { + ph.createPage().then(function(page) { + page.open(web.src).then(function(status) { + page.render(captureFile).then(function(){ + let tmpIMG=document.createElement('img') + tmpIMG.src=captureFile + tmpIMG.addEventListener('load',()=>{ + webImage.src=captureFile + webImage.style.height = tmpIMG.height+"px" + // webImage.style.width = tmpIMG.width+"px" + }) + }) + }) + }) + })*/ + web.addEventListener('dom-ready', ()=>{ + + /*let id=this.webviewID + let nextURL = this.urlList[id].url + let nextSource = this.urlList[id].source*/ + + // webImage.src='var/'+source+'.png' + // console.log('dom-ready') + web.setAttribute('isLoading','false') + if(web==this.currentWebview){ + // this.bufferPad.web.src=web.src + } + if(web!=this.currentWebview){ + } + /* this.currentWebviewBackground = web.parentElement + this.currentWebview = web + this.currentSource = source + this.arrangeWebviews()*/ + + let captureRect = { + x: 0, y: 0, + width: 2000, + height: 4000 + } + web.capturePage(captureRect,function(image) { + webImage.src=image.toDataURL() + console.log("web.capturePage",newUrl) + let buf = image.toPNG() + let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/'+source+'.png') + + fs.writeFile(captureFile, buf, function(err) { + if (err) { + console.log("ERROR Failed to save file", err) + } + else{ + } + }) + }) + /*this.numWebviewsReady++ + if(this.numWebviewsReady==7){ + this.arrangeWebviews() + TweenMax.to(this.frame,0.0,{opacity:"1"}) + }*/ + + /* web.capturePage((image)=>{ + // webImage.src='var/cosmic.png' + // console.log("Captured after Load",web.src) + webImage.src=image.toDataURL() + + /* let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/'+source+'.png') + let buf = image.toPng() + + fs.writeFile(captureFile, buf, function(err) { + if (err) { + console.log("ERROR Failed to save file", err) + } + else{ + } + })*/ + //}) + this.backButton.style.opacity = (web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (web.canGoForward()) ? 1 : 0.5 + // webImage.style.display="inline" + // web.openDevTools() + }) + + web.addEventListener('new-window', (e) => { + this.appendWebView(e.url) + console.log("this.currentSource",source) + this.urlList.push({'url':e.url,'source':source}) + }) + + overlay.addEventListener('click',(e)=>{ + // console.log("Overlay wurde geklickt",this.currentWebview.src) + e.stopPropagation() + e.preventDefault() + this.currentWebview.capturePage((image)=>{ + console.log(this.webFrame.offsetHeight) + this.currentWebviewImage.style.height = this.webFrame.offsetHeight+"px"//"100%" + this.currentWebviewImage.src=image.toDataURL() + this.currentWebviewFrame = e.target.parentElement + // this.bringWebToFront() + e.target.style.visibility="hidden" + + this.currentSource=source + this.expandButton.setAttribute('source',source) + + this.currentWebviewFrame = e.target.parentElement + this.currentWebview = web + this.currentSource = source + this.currentWebviewImage = webImage + this.arrangeWebviews() + // this.bufferPad.web.src=web.src + + this.backButton.style.opacity = (web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (web.canGoForward()) ? 1 : 0.5 + }) + //e.target.parentElement.style.zIndex=this.zIndex + + }) + /*overlay.addEventListener('click',(e)=>{ + e.stopPropagation() + e.preventDefault() + this.currentWebviewBackground = e.target.parentElement + this.bringWebToFront() + e.target.style.visibility="hidden" + //e.target.parentElement.style.zIndex=this.zIndex + })*/ + + webImage.addEventListener('pointerenter',(e)=>{ + // TweenMax.to(webBackground, 0.1, {x:"+= 50"}) + }) + + webImage.addEventListener('pointerleave',(e)=>{ + // TweenMax.to(webBackground, 0.1, {x:"-= 50"}) + }) + + webImage.addEventListener('click',(e)=>{ + + this.currentSource=source + this.expandButton.setAttribute('source',source) + + // this.expandButton.style.opacity="0.5" + e.stopPropagation() + e.preventDefault() + let oldWeb=this.currentWebview + let oldWebImage = this.currentWebviewImage + + oldWeb.capturePage((image)=>{ + oldWebImage.src=image.toDataURL() + + this.currentWebviewBackground = e.target.parentElement + this.currentWebview = web + this.currentWebviewImage = webImage + this.currentSource = source + this.arrangeWebviews() + }) + + web.capturePage((image)=>{ + webImage.src=image.toDataURL() + + /* let captureFile=nodePath.join(nodeDirname, '../dev/tumortisch/var/'+source+'.png') + let buf = image.toPNG() + + fs.writeFile(captureFile, buf, function(err) { + if (err) { + console.log("ERROR Failed to save file", err) + } + else{ + } + })*/ + }) + + // this.bufferPad.web.src=web.src + + this.backButton.style.opacity = (web.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (web.canGoForward()) ? 1 : 0.5 + // e.target.style.visibility="hidden" + //e.target.parentElement.style.zIndex=this.zIndex + }) + + /*overlay.addEventListener('pointerenter',(e)=>{ + TweenMax.to(webBackground, 0.1, {x:"+= 20"}) + }) + + overlay.addEventListener('pointerleave',(e)=>{ + TweenMax.to(webBackground, 0.1, {x:"-= 20"}) + })*/ + this.arrangeWebviews() + } + + arrangeWebviews(){ + let numWebviews = this.webviewMap.size + let index = 0 + let increment = 1 + let z = this.zIndex + let current=this.currentWebviewFrame + let shadowWidth = 0 + this.webviewMap.forEach(function (entry, key) { + entry.style.maxWidth="1000px" + // entry.style.width=100-Math.sqrt(entry.parentElement.offsetWidth-590)+"%" + entry.style.width="70%" + shadowWidth = $(entry).find(".accordionWebviewShadow").outerWidth() + // console.log("entry.parentElement.offsetWidth",entry.parentElement.offsetWidth) + z+=increment + let offset=index*(entry.parentElement.offsetWidth+2*shadowWidth-entry.offsetWidth)/(numWebviews-1) + // console.log("Offset",entry.parentElement.offsetWidth-(offset+entry.offsetWidth)) + entry.style.left=entry.parentElement.offsetWidth+2*shadowWidth-(offset+entry.offsetWidth)-shadowWidth+"px" + entry.style.zIndex=z + // $( entry ).children(".accordionWebview[isLoading='false']").css( "visibility", "hidden" ) + $( entry ).children(".accordionWebview").css( "visibility", "hidden" ) + /* $( entry ).children(".accordionWebview").css( "visibility", "hidden" ).each(function(i, obj) { + console.log("isLoading()",obj.isLoading()) + })*/ + $( entry ).children('.accordionOverlay').css( "visibility", "visible" ) + $( entry ).children('.accordionWebimage').css( "visibility", "visible" ) + // entry.style.left=index*(entry.parentElement.offsetWidth-entry.offsetWidth)/(numWebviews-1)+"px" + index++ + if(entry==current){ + $( entry ).children('.accordionWebview').css( "visibility", "visible" ) + $( entry ).children('.accordionOverlay').css( "visibility", "hidden" ) + $( entry ).children('.accordionWebimage').css( "visibility", "hidden" ) + increment*=-1 + // entry.childNodes[1].style.opacity=0.8 + } + }) + this.zIndex=z + + // this.enableButtons() + /*this.zIndex++ + this.currentWebviewBackground.style.zIndex=this.zIndex + $( this.currentWebviewBackground ).children('.accordionWebview').css( "visibility", "visible" ) + $( this.currentWebviewBackground ).children('.accordionOverlay').css( "visibility", "hidden" ) + $( this.currentWebviewBackground ).children('.accordionWebimage').css( "visibility", "hidden" )*/ +} + + bringWebToFront(){ + this.arrangeWebviews() + this.zIndex++ + this.currentWebviewFrame.style.zIndex=this.zIndex + // this.currentWebviewBackground.childNodes[2].style.opacity=0.8 + } + rad2degree(alpha){ + return alpha * 180 / Math.PI; + } + + degree2rad(alpha){ + return alpha * Math.PI / 180; + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + urlPadMap.delete(this.title) + let geneTab = document.getElementById(this.title +"_tab") + TweenMax.to(geneTab, 0.4, {scaleX:"1.0",x:"0px"}); + geneTab.style.boxShadow = "none" + geneTab.setAttribute('open','false') + } + + enableButtons() { + this.backButton.style.opacity = (this.currentWebview.canGoBack()) ? 1 : 0.5 + this.forwardButton.style.opacity = (this.currentWebview.canGoForward()) ? 1 : 0.5 + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value="close" + button.draggable = false + this.frame.appendChild(button) + return button + } + + layout() { + console.log("LAYOUT") + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + let h = "calc(100% - " + (b) +"px)" + let w = "calc(100% - "+2 +"px)" + + this.scatter.resizeButton.style.width=b+"px" + this.scatter.resizeButton.style.height=b+"px" + + Elements.setStyle(this.webFrame, { + width: w, + height: h, + margin: "1px"}) + /*Elements.setStyle(this.webBackground, { + width: size, + height: size, + margin: b+"px"}) + + Elements.setStyle(this.overlay, { + width: size, + height: size, + margin: b+"px"})*/ + + Elements.setStyle(this.titlebar, { + left: (b * 5.75) + "px", + bottom: "0px", + fontSize: 0.7*b+"px", + // paddingTop: 10+"px", + // textAlign: "center", + margin: 0.1*b+"px", + height: 0.8*b+"px"}) + Elements.setStyle(this.closeAllButton, { + right: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.backButton, { + left: (b * 0.8) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.forwardButton, { + left: (this.border + (b * 0.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.expandButton, { + left: (this.border + (b * 1.8)) +"px", + bottom: "0px", + // opacity: "0.5", + margin: 0.1*b+"px", + width: 0.8*b+"px", + height: 0.8*b+"px"}) + Elements.setStyle(this.closeButton, { + left: (this.border + (b * 2.8)) +"px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + } + +} + +class PadFromElement { + + constructor(element, scatterContainer, { + startScale=1.0, minScale=0.1, maxScale=1.0, + autoBringToFront=true, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + type = 'fromElement', + title = 'New Pad', + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.element = element + this.type = type + this.title = title + this.x = x + this.y = y + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + Elements.setStyle(this.frame, { + width: this.width+"px", + height: this.height+"px", + backgroundColor: "#333", + position: "fixed", + visibility: "visible", + top: 0, + left: 0, + overflow: "auto"}) + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + + document.body.appendChild( this.frame) + this.border = 50 + + this.frame.appendChild(this.element) + + this.title = document.createElement("div") + this.title.innerHTML = "Titel" + this.title.style.color = "white" + this.frame.appendChild(this.title) + + Elements.setStyle(this.title, { + position: "absolute" + }) + // this.element.style.overflow = "auto" + // this.element.style.position = "absolute" + + this.layout() + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.frame.style.visibility="hidden" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + this.scatter.bringToFront() + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.element.addEventListener('pointerdown', (e) => { + this.scatter.bringToFront() + }) + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + // urlPadMap.delete(this.url) + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value="close" + button.draggable = false + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + let size = "calc(100% - " + (2*b+2) +"px)" + Elements.setStyle(this.element, { + width: size, + height: size, + top: b+"px", + left: b+"px"}) + Elements.setStyle(this.closeButton, { + right: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + Elements.setStyle(this.title, { + left: (b * 1.5) + "px", + fontSize: (b * 0.8) + "px", + top: (0.1)+"0px"}) + } +} +class minimalPad { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + type = 'minimal', + title="new Pad", + urlList=["https://www.iwm-tuebingen.de/www/index.html"], + padMapKey="key", + // url="https://www.iwm-tuebingen.de/www/index.html", + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + let w = window, + d = document, + e = d.documentElement, + g = d.getElementsByTagName('body')[0] + this.pageWidth = w.innerWidth || e.clientWidth || g.clientWidth, + this.pageHeight = w.innerHeight|| e.clientHeight|| g.clientHeight + + this.x = x + this.y = y + this.type = type + // this.url = url + this.urlList = urlList + this.padMapKey = padMapKey, + this.title = title, + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.web = null + this.WebviewCounter = 0 + this.id=0 + + this.webviewMap = new Map() + this.overlayMap = new Map() + this.tabMap = new Map() + this.listMap = new Map() + + this.frame = document.createElement('div') + this.frame.classList.add("pad") + this.border = this.pageHeight*0.023148 / startScale + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + display: 'flex', + width: this.width+"px", + height: this.height+"px", + visibility: "visible", + top: 0, + left: 0, + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + overflow: "visible"}) + + document.body.appendChild( this.frame) + + this.tabs=document.createElement("div") + this.webBackground=document.createElement("div") + this.webviewList=document.createElement("div") + this.titlebar = document.createElement("div") + this.webViewSnapshot=document.createElement("img") + + this.tabs.classList.add("interactiveElement") + this.tabs.classList.add("tabs") + // this.tabs.style.display="flex" + // this.tabs.style.flexFlow="row nowrap" + + Elements.setStyle(this.tabs, { + position: "absolute", + background: "white", + overflowX: "auto", + overflowY: "hidden", + display: "flex", + flexFlow:"row nowrap", + // justifyContent: "flex-end", + alignItems: "flex-end" + // overflow: "auto", + }) + + this.titlebar.innerHTML = this.title + Elements.setStyle(this.titlebar, { + color: "#fff", + position: "absolute", + overflowX: "hidden" + }) + + Elements.setStyle(this.webBackground, { + position: "absolute", + overflow: "hidden", + background: "white" + }) + + this.webviewList.classList.add("interactiveElement") + + Elements.setStyle(this.webviewList, { + position: "absolute", + visibility: "hidden", + overflow: "auto", + background: "white", + boxShadow: "5px 5px 10px #bbb" + }) + + /*Elements.setStyle(this.overlay, { + position: "absolute", + overflow: "auto", + background: "white", + opacity: "0.8" + })*/ + + Elements.setStyle(this.webViewSnapshot, { + position: "absolute", + overflow: "auto" + }) + + this.frame.appendChild(this.titlebar) + this.frame.appendChild(this.webBackground) + // this.frame.appendChild(this.webViewSnapshot) + this.frame.appendChild(this.tabs) + this.frame.appendChild(this.webviewList) + + for(var i=0; i{ + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameTop.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameLeft.addEventListener('pointerenter',(e)=>{ + // e.target.setPointerCapture(e.pointerId) + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameLeft.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameRight.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameRight.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + /*this.web.addEventListener('new-window', (e) => { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new minimalPad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + })*/ + + this.backButton.addEventListener('click', (e)=>{ + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + this.showWebviewListButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.webviewList.style.visibility=="visible" ? this.webviewList.style.visibility="hidden" : this.webviewList.style.visibility="visible" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.scatter.addTransformEventCallback((e) => { + let newBorder = this.pageHeight*0.023148 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.layout() + + /*this.test = document.createElement('div') + + Elements.setStyle(this.test, { + position: "absolute", + width: "400px", + height: "400px", + overflow: "auto", + background: "pink" + }) + this.frame.appendChild(this.test) + + this.test.addEventListener('click',()=>{ + // this.frame.style.visibility="hidden" + console.log($( this.webBackground ).children()) + $( this.webBackground ).children().css( "visibility", "hidden" ) + $( this.frame ).children().css( "visibility", "hidden" ) + })*/ + + } + + addNewWebview(src){ + /* if(this.web!=null){ + this.web.style.visibility="hidden" + this.web.setAttribute('selected','0') + }*/ + + let webview=document.createElement("webview") + let progressBar = document.createElement('div') + webview.setAttribute("plugins",true) + this.web=webview + webview.setAttribute('loaded','0') + this.webBackground.appendChild(webview) + this.webviewMap.set(src+this.padMapKey, webview) + // console.log("number of webviews",this.webviewMap.size) + // $("webview").each(function(i, obj) { + // obj.setAttribute('selected','0') + // }) + + Elements.setStyle(webview, { + position: "absolute", + overflow: "auto", + width: "100%", + height: "100%", + // border: "1px solid #fff" + }) + + Elements.setStyle(progressBar, { + position: "absolute", + overflow: "hidden", + top: "0%", + left: "10%", + visibility: "hidden", + background: "white", + filter: "blur(50px)", + opacity: "0.2", + // pointerEvents: 'none', + width: "40%", + height: "100%" + // opacity: "0.1" + }) + + webview.src=src + webview.preload= path.join(__dirname, './preloadPad.js') + + let listItem=document.createElement('div') + listItem.className="interactiveElement" + listItem.classList.add("webviewListItem") + listItem.setAttribute('id',src+this.padMapKey) + // listItem.style.height = "5%" + // // listItem.style.width = "80%" + // listItem.style.marginLeft="10%" + // listItem.style.marginTop="5%" + listItem.style.padding = "10px" + // listItem.style.paddingBottom = "10px" + this.webviewList.appendChild(listItem) + this.listMap.set(src+this.padMapKey,listItem) + + webview.setAttribute('id',src+this.padMapKey) + + this.setSelectedWebview(src+this.padMapKey) + + this.addNewTab(webview,src) + + listItem.addEventListener('click',(e)=>{ + console.log(e.target.getAttribute('id')) + this.setSelectedWebview(e.target.getAttribute('id')) + this.webviewList.style.visibility = "hidden" + $(this.tabs).animate({scrollLeft: $(".tab[id='"+e.target.getAttribute('id')+"']").prop('offsetLeft') - 10}, "slow") + this.enableButtons() + }) + + listItem.addEventListener('pointerenter',(e)=>{ + e.target.style.background = "#ddd" + let webview=null + this.webviewMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + if(item.getAttribute('selected')=='0')item.style.opacity="0.5" + webview=item + // obj.style.width="50%" + // obj.style.height="50%" + } + }) + this.overlayMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + } + }) + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + }) + + listItem.addEventListener('pointerleave',(e)=>{ + e.target.style.background = "#fff" + if(e.target.getAttribute('selected')=='1') e.target.style.background = "#999" + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('selected')=='0'){ + item.style.visibility="hidden" + } + if(item.getAttribute('selected')=='1'){ + item.style.visibility="visible" + item.style.opacity="1" + } + // obj.style.width="100%" + // obj.style.height="100%" + }) + }) + let overlay = document.createElement('img') + overlay.style.position="absolute" + overlay.style.width="100%" + overlay.style.height="100%" + overlay.style.top="0px" + overlay.style.left="0px" + // overlay.style.background="white" + overlay.classList.add("minimalPadOverlay") + overlay.src="img/overlay.png" + + this.overlayMap.set(src,overlay) + + let loadAnim = document.createElement("div") + loadAnim.style.webkitAnimation= "spin 2s linear infinite" + loadAnim.style.animation= "spin 2s linear infinite" + loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule("div.tabs::-webkit-scrollbar {display: none;}"); + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + // overlay.appendChild(loadAnim) + + this.webBackground.appendChild(overlay) + this.webBackground.appendChild(progressBar) + + webview.addEventListener('did-navigate', (e) => { + this.enableButtons() + //this.backButton.style.opacity = (webview.canGoBack()) ? 1 : 0.5 + //this.forwardButton.style.opacity = (webview.canGoForward()) ? 1 : 0.5 + }) + + webview.addEventListener('dom-ready',()=>{ + // console.log("DOM READY TABBED WEBVIEW!!!!!!!!!!") + listItem.innerHTML=webview.getTitle()+" "+src + $(".tab[id='"+src+this.padMapKey+"']").children()[0].innerHTML=webview.getTitle() + // $(".tab[id='"+src+"']").children()[0].innerHTML="READY LOADED" + $(this.tabs).animate({scrollLeft: $(".tab[id='"+src+"']").prop('offsetLeft') - 10}, "slow") + }) + + webview.addEventListener('ipc-message', (e) => { + if(e.channel=='webviewPointerDown')this.scatter.bringToFront() + }) + + let obj = { blur: 2 } + let loadAnimation = new TimelineMax({repeat:-1}) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 5,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + // loadAnimation.add( TweenMax.to(obj, 0.5, {blur: 2,onUpdate: () => {web.style.filter = "blur(" + obj.blur + "px)"}}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"60%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0, {left:"20%"}) ) + loadAnimation.add( TweenMax.to(progressBar, 0.5, {left:"60%"}) ) + loadAnimation.stop() + + webview.addEventListener('did-start-loading', ()=>{ + console.log("started loading page") + + webview.setAttribute('loaded','0') + overlay.style.visibility="visible" + webview.style.filter = "blur(10px)" + progressBar.style.visibility="visible" + loadAnimation.play() + + let w = overlay.offsetWidth + let h = overlay.offsetHeight + + /*let animationSize = w{ + loadAnimation.stop() + webview.style.filter = "blur(0px)" + webview.setAttribute('loaded','1') + this.layout() + progressBar.style.visibility="hidden" + overlay.style.visibility="hidden" + }) + + webview.addEventListener('new-window', (e) => { + if (!this.webviewMap.has(e.url+this.padMapKey)){ + this.addNewWebview(e.url) + } + }) + this.id++ + } + + setSelectedWebview(id){ + let webview=null + this.webviewMap.forEach(function (item, key, mapObj) { + item.setAttribute('selected','0') + item.style.opacity = "0" + item.style.visibility = "hidden" + if(key==id){ + webview=item + item.setAttribute('selected','1') + item.style.opacity = "1" + item.style.visibility = "visible" + } + }) + + this.tabMap.forEach(function (tab, key, mapObj) { + tab.style.border="none" + if(key==id){ + tab.style.borderLeft="1px solid black" + tab.style.borderTop="1px solid black" + tab.style.borderRight="1px solid black" + } + }) + + this.listMap.forEach(function (entry, key, mapObj) { + entry.style.background = "white" + entry.setAttribute('selected','0') + if(key==id){ + entry.style.background = "#999" + entry.setAttribute('selected','1') + } + }) + this.web = webview + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + } + + addNewTab(webview,src){ + + this.tabMap.forEach(function (tab, key, mapObj) { + tab.style.border="none" + }) + let tab=document.createElement('div') + tab.className="tab" + tab.classList.add("interactiveElement") + tab.style.display="flex" + tab.style.flexFlow="row nowrap" + tab.style.textOverflow="ellipsis" + tab.setAttribute('id',src+this.padMapKey) + // tab.innerHTML=" " + tab.style.color="black" + tab.style.background="white" + tab.style.borderLeft="1px solid black" + tab.style.borderTop="1px solid black" + tab.style.borderRight="1px solid black" + tab.style.position="relative" + tab.style.borderRadius="5px 5px 0 0" + tab.style.maxWidth="25%" + tab.style.height="80%" + tab.style.alignItems= "center" + // tab.style.display="inline" + // tab.style.marginLeft="10px" + tab.style.paddingLeft="10px" + this.tabs.appendChild(tab) + this.tabMap.set(src+this.padMapKey,tab) + let newSelectedWebview=null + + let title = document.createElement('div') + title.classList.add('tabTitle') + // title.style.display="flex" + // title.style.flexFlow="row nowrap" + title.style.overflow="hidden" + title.style.whiteSpace="nowrap" + title.style.textOverflow="ellipsis" + title.style.pointerEvents="none" + title.innerHTML="new Tab" + tab.appendChild(title) + + let close = document.createElement('img') + close.setAttribute('id',src+this.padMapKey) + close.classList.add('tabCloseButton') + close.style.background="#666" + close.style.height="50%" + close.style.marginLeft="10px" + close.style.marginRight="10px" + close.style.borderRadius="100%" + close.style.visibility="hidden" + close.src = "../../assets/icons/svg/cross.svg" + tab.appendChild(close) + + close.addEventListener('click',(e)=>{ + e.stopPropagation() + e.preventDefault() + let newSelectedItem = false + + if(this.webviewMap.size>1){ + let next=this.getNextTab(e.target.getAttribute("id")) + let previous=this.getPreviousTab(e.target.getAttribute("id")) + + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('id')==e.target.getAttribute("id")){ + if(item.getAttribute('selected')=='1'){ + newSelectedItem=true + } + mapObj.delete(key) + $(item).remove() + } + }) + + this.tabMap.forEach(function (tab, key, mapObj) { + if(tab.getAttribute('id')==e.target.getAttribute("id")){ + mapObj.delete(key) + $(tab).remove() + } + }) + + this.listMap.forEach(function (entry, key, mapObj) { + if(entry.getAttribute('id')==e.target.getAttribute("id")){ + mapObj.delete(key) + } + }) + $(".webviewListItem[id='"+e.target.getAttribute("id")+"']").remove() + $(e.target).remove() + if(newSelectedItem){ + next!=null ? this.setSelectedWebview(next.getAttribute('id')) : this.setSelectedWebview(previous.getAttribute('id')) + } + } + }) + + tab.addEventListener('pointerenter',(e)=>{ + if(this.webviewMap.size>1)e.target.childNodes[1].style.visibility="visible" + let z=this.frame.style.zIndex + + let webview=null + e.target.style.background="#ddd" + this.webviewMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + if(item.getAttribute('selected')=='0')item.style.opacity="0.5" + webview=item + // obj.style.width="50%" + // obj.style.height="50%" + } + }) + this.overlayMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + } + }) + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + /*Popup.open({"text":webview.getTitle()}, + {x: e.clientX, y: e.clientY}, + { fontSize: "2vh", maxWidth: width*0.2, spacing: '10px', notchPosition: 'topLeft', zIndex: z+100})*/ + }) + + tab.addEventListener('pointerleave',(e)=>{ + e.target.childNodes[1].style.visibility="hidden" + e.target.style.background="#fff" + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('selected')=='0'){ + item.style.visibility="hidden" + } + if(item.getAttribute('selected')=='1'){ + item.style.visibility="visible" + item.style.opacity="1" + } + // obj.style.width="100%" + // obj.style.height="100%" + }) + //this.web.style.visibility="visible" + //this.web.style.opacity="1" + }) + tab.addEventListener('click',(e)=>{ + this.tabMap.forEach(function (item, key, mapObj) { + item.style.border="none" + }) + + e.target.style.borderLeft="1px solid black" + e.target.style.borderTop="1px solid black" + e.target.style.borderRight="1px solid black" + + this.setSelectedWebview(e.target.getAttribute('id')) + + if(e.target.offsetLeft+e.target.offsetWidth>this.tabs.offsetWidth+this.tabs.scrollLeft){ + $(this.tabs).animate({scrollLeft: e.target.offsetLeft-this.tabs.offsetWidth+e.target.offsetWidth}, "slow") + } + if(e.target.offsetLeft{ + this.scatter.scalable=false + this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameTop.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameLeft.addEventListener('pointerenter',(e)=>{ + // e.target.setPointerCapture(e.pointerId) + this.scatter.scalable=false + this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameLeft.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameRight.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameRight.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + // this.element.style.overflow = "auto" + // this.element.style.position = "absolute" + + this.layout() + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.frame.style.visibility="hidden" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.bringToFront() + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.element.addEventListener('pointerdown', (e) => { + this.scatter.bringToFront() + }) + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + this.frame.appendChild(button) + return button + } + + addToolButton(src, value) { + console.log("adding tool button",value) + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value=value + button.draggable = false + this.frame.appendChild(button) + this.toolButtonMap.set(value,button) + return button + } + + layout() { + console.log("layouting minmalPadFromElement") + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + + let size = "calc(100% - " + (2*b) +"px)" + let h = "calc(100% - " + (b) +"px)" + let w = "calc(100% - "+2 +"px)" + let w2 = "calc(100% - " + (b+2) +"px)" + + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + + Elements.setStyle(this.element, { + width: w, + height: h, + top: 0+"px", + left: 0+"px"}) + + Elements.setStyle(this.pseudoFrameTop, { + width: w, + height: 0.1*b+"px", + margin: "0px"}) + Elements.setStyle(this.pseudoFrameLeft, { + width: 0.1*b+"px", + height: h, + margin: "0px"}) + Elements.setStyle(this.pseudoFrameRight, { + width: 0.1*b+"px", + height: h, + margin: "0px"}) + + Elements.setStyle(this.closeButton, { + right: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + let buttonCounter=0 + this.toolButtonMap.forEach(function (entry, key) { + // console.log(entry,key) + Elements.setStyle(entry, { + left: (b * (buttonCounter+0.75)) + "px", + bottom: "0px", + margin: 0.1*b+"px", + width: 0.8*b+"px", + height: 0.8*b+"px"}) + + buttonCounter++ + } + ) + + /*Elements.setStyle(this.addContentButton, { + left: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.copyButton, { + left: (b * 1.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.pasteButton, { + left: (b * 2.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.saveButton, { + left: (b * 3.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + + Elements.setStyle(this.deleteButton, { + left: (b * 4.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"})*/ + } +} + +module.exports = { Pad, minimalPad, DOMPadContainer, PadFromElement, minimalPadFromElement, PadAccordion } diff --git a/browser/padMinimal.js b/browser/padMinimal.js new file mode 100644 index 0000000..12be656 --- /dev/null +++ b/browser/padMinimal.js @@ -0,0 +1,1380 @@ +const {fileURL} = require('./utils.js') +const path = require('path') + +/* A specialization that ignores webview events and thus allows + * webviews to get touch, mouse and wheel events. + */ +class minimalDOMPadContainer extends DOMScatterContainer { + + capture(event) { + if (event.target.tagName === 'WEBVIEW' || event.target.classList.contains('interactiveElement')) + return false + return super.capture(event) + } +} + +/* A wrapper for a webview that behaves like a virtual tablet browser. + * Uses a DOMScatter to zoom and rotate the virtual browser window. + * The position of buttons and the border size remain constant. + */ +class minimalPad { + + constructor(scatterContainer, { + startScale=1.0, minScale=0.25, maxScale=10.5, + autoBringToFront=true, + type = 'minimal', + title="", + tabbedView = true, + hasTtitleBar = false, + // urlList=["https://www.iwm-tuebingen.de/www/index.html"], + padMapKey="key", + url="https://www.iwm-tuebingen.de/www/index.html", + hideOnStart=false, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.x = x + this.y = y + this.type = type + this.url = url + // this.urlList = urlList + this.padMapKey = padMapKey, + this.title = title, + this.hasTtitleBar = hasTtitleBar + this.tabbedView = tabbedView + this.hideOnStart = hideOnStart + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.startScale = startScale + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.web = null + this.WebviewCounter = 0 + this.id=0 + this.pointerCounter = 0 + + this.webviewMap = new Map() + this.overlayMap = new Map() + this.tabMap = new Map() + this.listMap = new Map() + + this.frame = document.createElement('div') + this.frame.classList.add("pad") + this.border = 50 / startScale + + Elements.setStyle(this.frame, { + backgroundColor: "#333", + position: "absolute", + display: 'flex', + width: this.width+"px", + height: this.height+"px", + visibility: "visible", + top: 0, + left: 0, + // boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`, + // borderRadius: '10px', + overflow: "visible"}) + + document.body.appendChild( this.frame) + + this.tabs=document.createElement("div") + this.webBackground=document.createElement("div") + this.webviewList=document.createElement("div") + this.titlebar = document.createElement("div") + this.webViewSnapshot=document.createElement("img") + + this.tabs.classList.add("interactiveElement") + this.tabs.classList.add("tabs") + // this.tabs.style.display="flex" + // this.tabs.style.flexFlow="row nowrap" + + Elements.setStyle(this.tabs, { + position: "absolute", + background: "white", + overflowX: "auto", + overflowY: "hidden", + display: "flex", + flexFlow:"row nowrap", + // justifyContent: "flex-end", + alignItems: "flex-end" + // overflow: "auto", + }) + + this.titlebar.innerHTML = "" + + Elements.setStyle(this.titlebar, { + color: "#fff", + position: "absolute", + overflowX: "hidden" + }) + + Elements.setStyle(this.webBackground, { + position: "absolute", + overflow: "hidden", + background: "white" + }) + + this.webviewList.classList.add("interactiveElement") + + Elements.setStyle(this.webviewList, { + position: "absolute", + visibility: "hidden", + overflow: "auto", + background: "white", + boxShadow: "5px 5px 10px #bbb" + }) + + /*Elements.setStyle(this.overlay, { + position: "absolute", + overflow: "auto", + background: "white", + opacity: "0.8" + })*/ + + Elements.setStyle(this.webViewSnapshot, { + position: "absolute", + overflow: "auto" + }) + + if(this.hasTtitleBar)this.frame.appendChild(this.titlebar) + this.frame.appendChild(this.webBackground) + // this.frame.appendChild(this.webViewSnapshot) + this.frame.appendChild(this.tabs) + this.frame.appendChild(this.webviewList) + + this.webViewSnapshot.style.visibility="hidden" + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + this.backButton = this.addButton("../assets/icons/svg/left.svg", "go back") + this.forwardButton = this.addButton("../assets/icons/svg/right.svg", "go forward") + this.showWebviewListButton = this.addButton("../assets/icons/svg/flat/ic_list_48px.svg", "show list") + + this.addNewWebview(this.url) + + this.backButton.style.opacity = 0.5 + this.forwardButton.style.opacity = 0.5 + + /*for (let callback of window.padLoadedHandler) { + callback(this, url) + }*/ + + // this.pseudoFrame = document.createElement('div') + this.pseudoFrameTop = document.createElement('div') + this.pseudoFrameLeft = document.createElement('div') + this.pseudoFrameRight = document.createElement('div') + + /*Elements.setStyle(this.pseudoFrame, { + position: "absolute", + top: "0px", + left: "0px", + width: "100%", + height: "100%", + pointerEvents: "none" + })*/ + + Elements.setStyle(this.pseudoFrameTop, { + position: "absolute", + // background: "pink", + top: "0px", + left: "0px", + width: "100%" + // height: "50px" + }) + + Elements.setStyle(this.pseudoFrameLeft, { + position: "absolute", + // background: "pink", + top: "0px", + left: "0px", + // width: "5px", + height: "100%" + }) + + Elements.setStyle(this.pseudoFrameRight, { + position: "absolute", + // background: "pink", + top: "0px", + right: "0px", + // width: "5px", + height: "100%" + }) + this.frame.appendChild(this.pseudoFrameTop) + this.frame.appendChild(this.pseudoFrameLeft) + this.frame.appendChild(this.pseudoFrameRight) + // this.frame.appendChild(this.pseudoFrame) + + this.pseudoFrameTop.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameTop.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameLeft.addEventListener('pointerenter',(e)=>{ + // e.target.setPointerCapture(e.pointerId) + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameLeft.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameRight.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.web.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameRight.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + /*this.web.addEventListener('new-window', (e) => { + + if(e.url.indexOf("youtube")>-1)return + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new minimalPad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + })*/ + + /*this.backButton.addEventListener('click', (e)=>{ + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + this.forwardButton.addEventListener('click', (e)=>{ + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + this.closeButton.addEventListener('click', (e)=>{ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + })*/ + + InteractionMapper.on('tap',this.backButton, e => { + if(this.web.canGoBack()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goBack() + } + }) + + InteractionMapper.on('tap',this.forwardButton, e => { + if(this.web.canGoForward()){ + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.web.goForward() + } + }) + + InteractionMapper.on('tap',this.closeButton, e => { + TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }}) + this.close() + }) + + this.showWebviewListButton.addEventListener('click', (e)=>{ + /*TweenMax.to(e.target, 0.1, {scale:"1.1", onComplete: ()=>{ + TweenMax.to(e.target, 0.1, {scale:"1.0"}) + }})*/ + this.webviewList.style.visibility=="visible" ? this.webviewList.style.visibility="hidden" : this.webviewList.style.visibility="visible" + }) + + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.moveTo({x, y}) + this.scatter.bringToFront() + + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + /*this.test = document.createElement('div') + + Elements.setStyle(this.test, { + position: "absolute", + width: "400px", + height: "400px", + overflow: "auto", + background: "pink" + }) + this.frame.appendChild(this.test) + + this.test.addEventListener('click',()=>{ + // this.frame.style.visibility="hidden" + console.log($( this.webBackground ).children()) + $( this.webBackground ).children().css( "visibility", "hidden" ) + $( this.frame ).children().css( "visibility", "hidden" ) + })*/ + this.heinz = document.createElement('div') + + Elements.setStyle(this.heinz, { + position: "absolute", + background: "pink", + top: "0px", + left: "0px", + width: "100%", + opacity: "0.5", + display: "inline", + height: "100%" + }) + + this.heinz.addEventListener('pointerdown',(e)=>{ + console.log('pointer down on Heinz') + e.target.style.display="none" + }) + this.layout() + + // this.webBackground.appendChild(this.heinz) + // this.heinz.classList.add("interactiveElement") + + } + + addNewWebview(src){ + /* if(this.web!=null){ + this.web.style.visibility="hidden" + this.web.setAttribute('selected','0') + }*/ + let timeTouchStart = 0 + + let webview=document.createElement("webview") + this.web=webview + // webview.style.pointerEvents="none" + webview.setAttribute('loaded','0') + this.webBackground.appendChild(webview) + this.webviewMap.set(src+this.padMapKey, webview) + // console.log("number of webviews",this.webviewMap.size) + // $("webview").each(function(i, obj) { + // obj.setAttribute('selected','0') + // }) + + Elements.setStyle(webview, { + position: "absolute", + overflow: "hidden", + width: "100%", + height: "100%", + // border: "1px solid #fff" + }) + webview.src=src + webview.preload= path.join(__dirname, './preloadPad.js') + + let listItem=document.createElement('div') + listItem.className="interactiveElement" + listItem.classList.add("webviewListItem") + listItem.setAttribute('id',src+this.padMapKey) + // listItem.style.height = "5%" + // // listItem.style.width = "80%" + // listItem.style.marginLeft="10%" + // listItem.style.marginTop="5%" + listItem.style.padding = "10px" + // listItem.style.paddingBottom = "10px" + this.webviewList.appendChild(listItem) + this.listMap.set(src+this.padMapKey,listItem) + + webview.setAttribute('id',src+this.padMapKey) + + this.setSelectedWebview(src+this.padMapKey) + + this.addNewTab(webview,src) + + listItem.addEventListener('click',(e)=>{ + console.log(e.target.getAttribute('id')) + this.setSelectedWebview(e.target.getAttribute('id')) + this.webviewList.style.visibility = "hidden" + $(this.tabs).animate({scrollLeft: $(".tab[id='"+e.target.getAttribute('id')+"']").prop('offsetLeft') - 10}, "slow") + this.enableButtons() + }) + + listItem.addEventListener('pointerenter',(e)=>{ + e.target.style.background = "#ddd" + let webview=null + this.webviewMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + if(item.getAttribute('selected')=='0')item.style.opacity="0.5" + webview=item + // obj.style.width="50%" + // obj.style.height="50%" + } + }) + this.overlayMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + } + }) + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + }) + + listItem.addEventListener('pointerleave',(e)=>{ + e.target.style.background = "#fff" + if(e.target.getAttribute('selected')=='1') e.target.style.background = "#999" + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('selected')=='0'){ + item.style.visibility="hidden" + } + if(item.getAttribute('selected')=='1'){ + item.style.visibility="visible" + item.style.opacity="1" + } + // obj.style.width="100%" + // obj.style.height="100%" + }) + }) + let overlay = document.createElement('div') + overlay.style.position="absolute" + overlay.style.width="100%" + overlay.style.height="100%" + overlay.style.top="0px" + overlay.style.left="0px" + overlay.style.background="white" + overlay.style.opacity="0.6" + + this.overlayMap.set(src,overlay) + + let overlayCaptureEvents = document.createElement('div') + overlayCaptureEvents.style.position="absolute" + overlayCaptureEvents.style.background="lime" + overlayCaptureEvents.style.opacity="0.01" + overlayCaptureEvents.style.width="100%" + overlayCaptureEvents.style.height="100%" + overlayCaptureEvents.style.top="0px" + overlayCaptureEvents.style.left="0px" + // overlay.style.background="white" + + overlayCaptureEvents.classList.add("interactiveElement") + + overlayCaptureEvents.addEventListener('touchmove',(e)=>{ + e.preventDefault() + e.stopPropagation() + }) + + overlayCaptureEvents.addEventListener('pointerup',(e)=>{ + e.preventDefault() + e.stopPropagation() + let p = {x:e.clientX, y:e.clientY} + + let webviewPosition = Points.fromPageToNode(webview,p) + // oldX = 0 + // oldY = 0 + + let d = new Date() + console.log("delta Time",d.getTime()-timeTouchStart) + // if(d.getTime()-timeTouchStart<150)webview.sendInputEvent({type:'mouseUp', x: (e.clientX-webview.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-webview.getBoundingClientRect().top)/this.scatter.scale, button:'left', clickCount: 1}) + if(d.getTime()-timeTouchStart<150)webview.sendInputEvent({type:'mouseUp', x: webviewPosition.x, y: webviewPosition.y, button:'left', clickCount: 1}) + }) + + overlayCaptureEvents.addEventListener('pointerdown',(e)=>{ + e.preventDefault() + e.stopPropagation() + this.scatter.bringToFront() + let p = {x:e.clientX, y:e.clientY} + + let webviewPosition = Points.fromPageToNode(webview,p) + + let d = new Date() + timeTouchStart = d.getTime() + + // webview.sendInputEvent({type:'mouseDown', x: (e.clientX-webview.getBoundingClientRect().left)/this.scatter.scale, y: (e.clientY-webview.getBoundingClientRect().top)/this.scatter.scale, button:'left', clickCount: 1}) + webview.sendInputEvent({type:'mouseDown', x: webviewPosition.x, y: webviewPosition.y, button:'left', clickCount: 1}) + }) + + overlayCaptureEvents.addEventListener('pointermove',(e)=>{ + if(e.pointerType!='mouse'){ + let rotation = Angle.radian2degree(this.scatter.rotation); + rotation = (rotation + 360) % 360; + + let r = Math.sqrt(Math.pow(e.movementX, 2) + Math.pow(e.movementY, 2)); + let phi = Angle.radian2degree(Math.atan2(e.movementX, e.movementY)); + + phi = ((phi) + 630) % 360; + let rot = ((rotation + 90) + 630) % 360; + + let diffAngle = ((0 + rot) + 360) % 360; + let phiCorrected = (phi + diffAngle + 360) % 360; + + let deltaX = r * Math.cos(Angle.degree2radian(phiCorrected)); + let deltaY = -r * Math.sin(Angle.degree2radian(phiCorrected)); + // if(oldX!=0)deltaX = (e.clientX-oldX) + // if(oldY!=0)deltaY = (e.clientY-oldY) + // console.log("e.movementY",e.movementY) + // webview.sendInputEvent({type:'mouseWheel', x: 0, y: 0, deltaX: e.movementX, deltaY: e.movementY, canScroll: true }) + webview.executeJavaScript("window.scrollTo(scrollX+"+(-1*deltaX)+", scrollY+"+ (-1*deltaY)+")") + // oldX = e.clientX + // oldY = e.clientY + } + }) + + overlayCaptureEvents.addEventListener('mousewheel',(e)=>{ + console.log("mousewheel",e.deltaY) + // webview.sendInputEvent({type:'mouseWheel', x: 0, y: 0, deltaX: e.deltaX, deltaY: -e.deltaY, canScroll: true }) + webview.executeJavaScript("window.scrollTo(scrollX+"+e.deltaX+", scrollY+"+ e.deltaY+")") + }) + + let loadAnim = document.createElement("div") + loadAnim.style.webkitAnimation= "spin 2s linear infinite" + loadAnim.style.animation= "spin 2s linear infinite" + loadAnim.style.position = "absolute" + + document.styleSheets[0].insertRule("div.tabs::-webkit-scrollbar {display: none;}"); + document.styleSheets[0].insertRule('\ + @keyframes spin {\ + from { transform: rotateZ(0deg); }\ + to { transform: rotateZ(360deg); }\ + }' + ) + + overlay.appendChild(loadAnim) + + this.webBackground.appendChild(overlay) + if(remote.getGlobal('multiUserMode'))this.webBackground.appendChild(overlayCaptureEvents) + + webview.addEventListener('did-navigate', (e) => { + this.enableButtons() + //this.backButton.style.opacity = (webview.canGoBack()) ? 1 : 0.5 + //this.forwardButton.style.opacity = (webview.canGoForward()) ? 1 : 0.5 + }) + + webview.addEventListener('dom-ready',()=>{ + // console.log("DOM READY TABBED WEBVIEW!!!!!!!!!!") + listItem.innerHTML=webview.getTitle()+" "+src + $(".tab[id='"+src+this.padMapKey+"']").children()[0].innerHTML=webview.getTitle() + // $(".tab[id='"+src+"']").children()[0].innerHTML="READY LOADED" + $(this.tabs).animate({scrollLeft: $(".tab[id='"+src+"']").prop('offsetLeft') - 10}, "slow") + this.titlebar.innerHTML=this.web.getTitle() + // this.web.openDevTools() + // webview.style.overflow= "auto" + // webview.style.overflowY= "hidden" + }) + + webview.addEventListener('ipc-message', (e) => { + if(e.channel=='touchStart'){ + // console.log("pointer down on webview",e.srcElement) + if(e.srcElement.src==webview.src)this.pointerCounter++ + this.scatter.bringToFront() + } + if(e.channel=='touchEnd'){ + // console.log("pointer up on webview") + if(e.srcElement.src==webview.src)this.pointerCounter-- + } + if(e.channel=='webviewPointerCancel'){ + this.heinz.style.display="inline" + } + // console.log("pointerCounter",this.pointerCounter) + }) + + /* let {ipcRenderer} = require('electron') + + ipcRenderer.on('touchStartWebview' ,function(event){ + console.log(event) + })*/ + + webview.addEventListener('did-start-loading', ()=>{ + + webview.setAttribute('loaded','0') + overlay.style.visibility="visible" + + let w = overlay.offsetWidth + let h = overlay.offsetHeight + + let animationSize = w{ + // }) + webview.addEventListener('did-stop-loading', ()=>{ + webview.setAttribute('loaded','1') + this.layout() + overlay.style.visibility="hidden" + }) + + webview.addEventListener('new-window', (e) => { + if (!this.webviewMap.has(e.url+this.padMapKey)){ + if(this.tabbedView)this.addNewWebview(e.url) + if(!this.tabbedView){ + + if (urlPadMap.has(e.url)) { + let childPad = urlPadMap.get(e.url) + childPad.scatter.moveTo(x, y) + return childPad + } + let childPad = new minimalPad(this.scatterContainer, { + x: this.scatter.position.x+100, + y: this.scatter.position.y+100, + url: e.url, + tabbedView: false, + hasTtitleBar: this.hasTtitleBar, + width: this.scatter.width, + height: this.scatter.height, + scalable: true, + rotatable: true}) + urlPadMap.set(e.url, childPad) + + for(let callback of window.padLoadedHandler) { + callback(childPad, url) + } + + } + } + }) + this.id++ + } + + setSelectedWebview(id){ + let webview=null + this.webviewMap.forEach(function (item, key, mapObj) { + item.setAttribute('selected','0') + item.style.opacity = "0" + item.style.visibility = "hidden" + if(key==id){ + webview=item + item.setAttribute('selected','1') + item.style.opacity = "1" + item.style.visibility = "visible" + } + }) + + this.tabMap.forEach(function (tab, key, mapObj) { + tab.style.border="none" + if(key==id){ + tab.style.borderLeft="1px solid black" + tab.style.borderTop="1px solid black" + tab.style.borderRight="1px solid black" + } + }) + + this.listMap.forEach(function (entry, key, mapObj) { + entry.style.background = "white" + entry.setAttribute('selected','0') + if(key==id){ + entry.style.background = "#999" + entry.setAttribute('selected','1') + } + }) + this.web = webview + if(webview.getAttribute('loaded')=='1') this.titlebar.innerHTML=this.web.getTitle() + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + } + + addNewTab(webview,src){ + + this.tabMap.forEach(function (tab, key, mapObj) { + tab.style.border="none" + }) + let tab=document.createElement('div') + tab.className="tab" + tab.classList.add("interactiveElement") + tab.style.display="flex" + tab.style.flexFlow="row nowrap" + tab.style.textOverflow="ellipsis" + tab.setAttribute('id',src+this.padMapKey) + // tab.innerHTML=" " + tab.style.color="black" + tab.style.background="white" + tab.style.borderLeft="1px solid black" + tab.style.borderTop="1px solid black" + tab.style.borderRight="1px solid black" + tab.style.position="relative" + tab.style.borderRadius="5px 5px 0 0" + tab.style.maxWidth="25%" + tab.style.height="80%" + tab.style.alignItems= "center" + // tab.style.display="inline" + // tab.style.marginLeft="10px" + tab.style.paddingLeft="10px" + this.tabs.appendChild(tab) + this.tabMap.set(src+this.padMapKey,tab) + let newSelectedWebview=null + + let title = document.createElement('div') + title.classList.add('tabTitle') + // title.style.display="flex" + // title.style.flexFlow="row nowrap" + title.style.overflow="hidden" + title.style.whiteSpace="nowrap" + title.style.textOverflow="ellipsis" + title.style.pointerEvents="none" + title.innerHTML="New Tab" + tab.appendChild(title) + + let close = document.createElement('img') + close.setAttribute('id',src+this.padMapKey) + close.classList.add('tabCloseButton') + close.style.background="#666" + close.style.height="50%" + close.style.marginLeft="10px" + close.style.marginRight="10px" + close.style.borderRadius="100%" + close.style.visibility="hidden" + close.src = "../../assets/icons/svg/cross.svg" + tab.appendChild(close) + + close.addEventListener('click',(e)=>{ + e.stopPropagation() + e.preventDefault() + let newSelectedItem = false + + if(this.webviewMap.size>1){ + let next=this.getNextTab(e.target.getAttribute("id")) + let previous=this.getPreviousTab(e.target.getAttribute("id")) + + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('id')==e.target.getAttribute("id")){ + if(item.getAttribute('selected')=='1'){ + newSelectedItem=true + } + mapObj.delete(key) + $(item).remove() + } + }) + + this.tabMap.forEach(function (tab, key, mapObj) { + if(tab.getAttribute('id')==e.target.getAttribute("id")){ + mapObj.delete(key) + $(tab).remove() + } + }) + + this.listMap.forEach(function (entry, key, mapObj) { + if(entry.getAttribute('id')==e.target.getAttribute("id")){ + mapObj.delete(key) + } + }) + $(".webviewListItem[id='"+e.target.getAttribute("id")+"']").remove() + $(e.target).remove() + if(newSelectedItem){ + next!=null ? this.setSelectedWebview(next.getAttribute('id')) : this.setSelectedWebview(previous.getAttribute('id')) + } + } + this.layout() + + }) + + tab.addEventListener('pointerenter',(e)=>{ + if(this.webviewMap.size>1)e.target.childNodes[1].style.visibility="visible" + let z=this.frame.style.zIndex + + let webview=null + e.target.style.background="#ddd" + this.webviewMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + if(item.getAttribute('selected')=='0')item.style.opacity="0.5" + webview=item + // obj.style.width="50%" + // obj.style.height="50%" + } + }) + this.overlayMap.forEach(function (item, key, mapObj) { + item.style.visibility="hidden" + if(item.getAttribute('id')==e.target.getAttribute("id")){ + item.style.visibility="visible" + } + }) + // webview.getAttribute('loaded')=='0' ? this.overlay.style.visibility='visible' : this.overlay.style.visibility='hidden' + /*Popup.open({"text":webview.getTitle()}, + {x: e.clientX, y: e.clientY}, + { fontSize: "2vh", maxWidth: width*0.2, spacing: '10px', notchPosition: 'topLeft', zIndex: z+100})*/ + }) + + tab.addEventListener('pointerleave',(e)=>{ + e.target.childNodes[1].style.visibility="hidden" + e.target.style.background="#fff" + this.webviewMap.forEach(function (item, key, mapObj) { + if(item.getAttribute('selected')=='0'){ + item.style.visibility="hidden" + } + if(item.getAttribute('selected')=='1'){ + item.style.visibility="visible" + item.style.opacity="1" + } + // obj.style.width="100%" + // obj.style.height="100%" + }) + //this.web.style.visibility="visible" + //this.web.style.opacity="1" + }) + tab.addEventListener('click',(e)=>{ + this.tabMap.forEach(function (item, key, mapObj) { + item.style.border="none" + }) + + e.target.style.borderLeft="1px solid black" + e.target.style.borderTop="1px solid black" + e.target.style.borderRight="1px solid black" + + this.setSelectedWebview(e.target.getAttribute('id')) + + if(e.target.offsetLeft+e.target.offsetWidth>this.tabs.offsetWidth+this.tabs.scrollLeft){ + $(this.tabs).animate({scrollLeft: e.target.offsetLeft-this.tabs.offsetWidth+e.target.offsetWidth}, "slow") + } + if(e.target.offsetLeft 1){ + this.tabs.style.display = "flex" + this.showWebviewListButton.style.display = "inline" + + if(this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + top: b+"px", + width: w, + height: size-b, + margin: "1px"}) + Elements.setStyle(this.titlebar, { + display: "inline"}) + } + if(!this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + width: w, + height: size, + margin: "1px"}) + } + } + if(this.tabMap.size <= 1){ + this.tabs.style.display = "none" + this.showWebviewListButton.style.display = "none" + + if(this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + top: b+"px", + width: w, + height: size, + margin: "1px"}) + Elements.setStyle(this.titlebar, { + display: "inline"}) + } + if(!this.hasTtitleBar){ + Elements.setStyle(this.webBackground, { + width: w, + height: h, + margin: "1px"}) + } + } + + if(!this.tabbedView){ + this.tabs.style.display = "none" + this.showWebviewListButton.style.display = "none" + Elements.setStyle(this.webBackground, { + width: w, + height: h, + margin: "1px"}) + } + } + +} + +class minimalPadFromElement { + + constructor(element, scatterContainer, { + startScale=1.0, minScale=0.1, maxScale=1.0, + autoBringToFront=true, + translatable=true, scalable=true, rotatable=true, + movableX=true, + movableY=true, + rotationDegrees=null, + rotation=null, + onTransform=null, + transformOrigin = 'center center', + // extras which are in part needed + x=0, + y=0, + width=null, + height=null, + resizable=false, + } ={}) { + + this.element = element + + this.x = x + this.y = y + this.width = width + this.height = height + this.minScale = minScale + this.maxScale = maxScale + this.scatterContainer = scatterContainer + this.scale = startScale + this.scalable = scalable + this.rotatable = rotatable + this.rotationDegrees = this.startRotationDegrees + this.transformOrigin = transformOrigin + + this.frame = document.createElement('div') + Elements.setStyle(this.frame, { + width: this.width+"px", + height: this.height+"px", + backgroundColor: "#333", + position: "fixed", + top: 0, + left: 0, + overflow: "auto"}) + + this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close") + + document.body.appendChild( this.frame) + this.border = 50 + + this.frame.appendChild(this.element) + + this.title = document.createElement("div") + this.title.innerHTML = "" + this.title.style.color = "white" + // this.frame.appendChild(this.title) + + Elements.setStyle(this.title, { + position: "absolute" + }) + + // this.pseudoFrame = document.createElement('div') + this.pseudoFrameTop = document.createElement('div') + this.pseudoFrameLeft = document.createElement('div') + this.pseudoFrameRight = document.createElement('div') + + /*Elements.setStyle(this.pseudoFrame, { + position: "absolute", + top: "0px", + left: "0px", + width: "100%", + height: "100%", + pointerEvents: "none" + })*/ + + Elements.setStyle(this.pseudoFrameTop, { + position: "absolute", + // background: "pink", + top: "0px", + left: "0px", + width: "100%" + // height: "50px" + }) + + Elements.setStyle(this.pseudoFrameLeft, { + position: "absolute", + // background: "pink", + top: "0px", + left: "0px", + // width: "5px", + height: "100%" + }) + + Elements.setStyle(this.pseudoFrameRight, { + position: "absolute", + // background: "pink", + top: "0px", + right: "0px", + // width: "5px", + height: "100%" + }) + this.frame.appendChild(this.pseudoFrameTop) + this.frame.appendChild(this.pseudoFrameLeft) + this.frame.appendChild(this.pseudoFrameRight) + // this.frame.appendChild(this.pseudoFrame) + + this.pseudoFrameTop.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameTop.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameLeft.addEventListener('pointerenter',(e)=>{ + // e.target.setPointerCapture(e.pointerId) + this.scatter.scalable=false + this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameLeft.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + + this.pseudoFrameRight.addEventListener('pointerenter',(e)=>{ + this.scatter.scalable=false + this.element.pointerEvents = "none" + e.target.style.cursor = "move" + }) + this.pseudoFrameRight.addEventListener('pointerleave',(e)=>{ + this.scatter.scalable=true + // this.frame.style.cursor = "initial" + }) + // this.element.style.overflow = "auto" + // this.element.style.position = "absolute" + + this.layout() + + this.closeButton.addEventListener('click', ()=>{ + this.frame.style.display="none" + }) + + this.scatter = new DOMScatter(this.frame, scatterContainer, { + x: this.x, + y: this.y, + startScale: this.startScale, + width: this.width, + height: this.height, + minScale: this.minScale, + maxScale: this.maxScale, + scalable: this.scalable, + resizable: true, + rotatable: this.rotatable}) + + let img=document.createElement("img") + img.style.width = "70%" + img.style.position = "absolute" + img.style.bottom = "20%" + img.style.right = "20%" + img.style.pointerEvents="none" + img.src = "../../assets/icons/png/flat/resize.png" + this.scatter.resizeButton.appendChild(img) + + this.scatter.bringToFront() + this.scatter.addTransformEventCallback((e) => { + let newBorder = 50 / e.scale + if (newBorder !== this.border) { + this.border = newBorder + this.layout() + } + }) + + this.element.addEventListener('pointerdown', (e) => { + this.scatter.bringToFront() + }) + } + + close() { + // this.frame.style.display="none" + this.frame.parentNode.removeChild(this.frame) + urlPadMap.delete(this.url) + } + + addButton(src, value) { + let button = document.createElement("img") + button.type = "image" + button.className = "frameButton" + button.style.position = "absolute" + button.src = fileURL(src) + button.value="close" + button.draggable = false + this.frame.appendChild(button) + return button + } + + layout() { + let b = this.border + let b2 = b * 2 + let b8 = b / 8 + let b25 = b / 25 + let b15 = b / 15 + + let size = "calc(100% - " + (2*b) +"px)" + let h = "calc(100% - " + (b) +"px)" + let w = "calc(100% - "+2 +"px)" + let w2 = "calc(100% - " + (b+2) +"px)" + + this.scatter.resizeButton.style.width=b+"px" + this.scatter.resizeButton.style.height=b+"px" + + Elements.setStyle(this.frame, { + // borderRadius: b8 + "px", + // boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)` + }) + + Elements.setStyle(this.element, { + width: w, + height: h, + top: 0+"px", + left: 0+"px"}) + + Elements.setStyle(this.title, { + left: (b * 1.5) + "px", + fontSize: (b * 0.8) + "px", + top: (0.1)+"0px"}) + + Elements.setStyle(this.pseudoFrameTop, { + width: w, + height: 0.1*b+"px", + margin: "0px"}) + Elements.setStyle(this.pseudoFrameLeft, { + width: 0.1*b+"px", + height: h, + margin: "0px"}) + Elements.setStyle(this.pseudoFrameRight, { + width: 0.1*b+"px", + height: h, + margin: "0px"}) + Elements.setStyle(this.closeButton, { + right: (b * 0.75) + "px", + bottom: "0px", + width: b+"px", + height: b+"px"}) + } +} + +module.exports = { minimalPad, minimalDOMPadContainer, minimalPadFromElement} diff --git a/browser/preload.js b/browser/preload.js new file mode 100644 index 0000000..b34bc67 --- /dev/null +++ b/browser/preload.js @@ -0,0 +1,231 @@ +const { fileURL, loadScript, hideCursor, showCursor } = require('./utils.js') +let { remote } = require('electron') + +const webFrame = require('electron').webFrame + +// UO: Disable unintended zoom of fullscreen page if user wants to zoom +// only parts like Eyevisit info cards. +console.log('Disable pinch zoom', webFrame) +webFrame.setVisualZoomLevelLimits(1, 1) + +let padContainer = null +let hideCursorTimeout = null +let debug = false +let urlPadMap = new Map() + +window.urlPadMap = urlPadMap +window.padLoadedHandler = [] +window.nodeDirname = __dirname + +function pageSize() { + var w = window, + d = document, + e = d.documentElement, + g = d.getElementsByTagName('body')[0], + width = w.innerWidth || e.clientWidth || g.clientWidth, + height = w.innerHeight || e.clientHeight || g.clientHeight + + return [width, height] +} + +let size = pageSize() +let pageWidth = size[0] +let pageHeight = size[1] + +/* Open a new at x, y position. */ +function openPad(url, x, y) { + console.log('openPad') + if (remote.getGlobal('useBrowserView')) { + return ipcRenderer.send('loadBrowserView', { url, x, y }) + } + const { Pad } = require('./pad.js') + const { minimalPad } = require('./padMinimal.js') + pad = null + if (urlPadMap.has(url)) { + let pad = urlPadMap.get(url) + pad.scatter.bringToFront() + /*TweenMax.to(pad.frame, 0.5, { + boxShadow: "0 0 25px 5px white", onComplete: () => { + TweenMax.to(pad.frame, 0.5, { boxShadow: "none" }) + } + })*/ + TweenMax.to(pad.frame, 0.2, { + scale: '1.01', + onComplete: () => { + TweenMax.to(pad.frame, 0.2, { scale: '1' }) + } + }) + // pad.scatter.moveTo(x, y) + return pad + } + y + 1600 > pageHeight ? (y = pageHeight - 1600) : (y = y) + if (remote.getGlobal('useMinimalPad')) { + pad = new minimalPad(padContainer, { + x: x, + y: y, + url: url, + tabbedView: true, + hasTtitleBar: false, + hideOnStart: false, + startScale: 1, + width: 1000, + height: 1500, + scalable: true, + rotatable: true + }) + } + if (!remote.getGlobal('useMinimalPad')) { + pad = new Pad(padContainer, { + x: x, + y: y, + url: url, + hideOnStart: false, + startScale: 1, + width: 1000, + height: 1500, + scalable: true, + rotatable: true + }) + } + urlPadMap.set(url, pad) + for (let callback of window.padLoadedHandler) { + callback(pad, url) + } +} + +window.padLoadedHandler.push((pad, url) => { + console.log('Add specific behavior') +}) + +/* According to https://electron.atom.io/docs/faq/ +"I can not use jQuery/RequireJS/Meteor/AngularJS in Electron" we +have to rename the symbols in the page before including other libraries. +Remember to use nodeRequire after this point. +*/ +window.nodeRequire = require +delete window.require +delete window.exports +delete window.module + +/* Create a DOMPadContainer, i.e. a special DOMScatterContainer, as a wrapper +of the document body. +*/ +window.addEventListener('load', e => { + console.log('preloading') + // ../iwmlib/dist/iwmlib.3rdparty.js + // loadScript('../iwmlib/lib/3rdparty/preload.js', () => { + loadScript('../../iwmlib/dist/iwmlib.3rdparty.preload.js', () => { + //console.log("../iwmlib/dist/iwmlib.3rdparty.js loaded") + console.log('greensock loaded') + // loadScript('../iwmlib/dist/iwmlib.js', () => { + loadScript('../../iwmlib/dist/iwmlib.js', () => { + console.log('../iwmlib/dist/iwmlib.js loaded') + + /* const { Pad, DOMPadContainer } = nodeRequire('./pad.js') + padContainer = new DOMPadContainer(document.body) + + window.nodePadContainer = padContainer + ipcRenderer.send('padContainerLoaded') */ + + /*Register a handler for mousemove events. Hide the cursor a few + *seconds after the last move event. + */ + document.body.addEventListener('mousemove', e => { + showCursor() + clearTimeout(hideCursorTimeout) + hideCursorTimeout = setTimeout(hideCursor, 3000) + }) + }) + + if (debug) { + loadScript('../iwmlib/dist/iwmlib.pixi.js', () => { + console.log('../iwmlib/dist/iwmlib.pixi.js loaded') + const DebugApp = require('./debug.js') + let debugApp = new DebugApp(document.body) + debugApp.setup() + debugApp.run() + }) + } + }) +}) + +/* Register a handler for all click events and check whether the link target +opens a new window. If the link target is blank, prevent the default behavior +and create a Pad scatter object instead. +*/ +window.addEventListener('click', e => { + let node = e.target + let url = '' + let target = '' + + let multiUserMode = false // remote.getGlobal('multiUserMode') // DOMScatter && remote.getGlobal('multiUserMode') + + // console.log("click", multiUserMode, remote.getGlobal('multiUserMode')) + if (multiUserMode) { + while (node !== null) { + if (node.tagName === 'A' || node.tagName === 'a') { + if (node.target instanceof SVGAnimatedString) { + url = node.href.baseVal + target = node.target.baseVal + } else { + url = node.href + target = node.target + } + if (target === '_blank') { + e.preventDefault() + openPad(url, e.clientX, e.clientY) + } + return + } + node = node.parentNode + } + } +}) + +/* Register a handler for contextmenu events and check whether the link target +// opens a new window. If the link target is blank, prevent the default behavior +and show a popup menu with the option to open a pad instead. +*/ +window.addEventListener('contextmenu', e => { + let node = e.target + let url = null + if (remote.getGlobal('multiUserMode')) { + while (node !== null) { + if (node.tagName === 'A' || node.tagName === 'a') { + if (node.target instanceof SVGAnimatedString) { + url = node.href.baseVal + } else { + url = node.href + } + e.preventDefault() + let point = { x: e.clientX, y: e.clientY } + PopupMenu.open( + { + 'Open new Pad': () => openPad(url, e.clientX, e.clientY) + }, + point, + { fontSize: '0.8em' } + ) + return + } + node = node.parentNode + } + } +}) + +/* Special error handling if the rendere process sends a error notification +log this error on the main console. +*/ +let { Console } = require('console') +let debugConsole = new Console(process.stdout, process.stderr) + +let { ipcRenderer } = require('electron') +window.addEventListener('error', event => { + debugConsole.error('PAGE ERROR', event.error, event.filename) + debugConsole.error('open', window.location.href) + ipcRenderer.send('error', 1) +}) + +ipcRenderer.on('newPad', (e, data) => { + openPad(data, 0, 0) +}) diff --git a/browser/preloadPad.js b/browser/preloadPad.js new file mode 100644 index 0000000..5c24f84 --- /dev/null +++ b/browser/preloadPad.js @@ -0,0 +1,83 @@ +let { remote } = require('electron') +let { ipcRenderer } = require('electron') + +const path = require('path') + +const webFrame = require('electron').webFrame +console.log('Disable pinch zoom', webFrame) +webFrame.setVisualZoomLevelLimits(1, 1) + +window.nodePath = path + +window.nodeDirname = __dirname + +window.nodeRequire = require +delete window.require +delete window.exports +delete window.module + +window.padLoadedHandler = [] + +let pointerCounter = 0 + +window.addEventListener('pointerdown', (e) => { + //e.preventDefault() + // console.log("ipcRenderer.sendToHost('webviewPointerDown')") + ipcRenderer.sendToHost('webviewPointerDown') +}) + +window.addEventListener('pointerup', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerUp')") + ipcRenderer.sendToHost('webviewPointerUp') +}) + +window.addEventListener('pointerenter', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerEnter')") + ipcRenderer.sendToHost('webviewPointerEnter') +}) + +window.addEventListener('pointercancel', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerCancel')") + ipcRenderer.sendToHost('webviewPointerCancel') +}) + +window.addEventListener('pointerleave', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerLeave')") + ipcRenderer.sendToHost('webviewPointerLeave') +}) + +window.addEventListener('pointerout', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerOut')") + ipcRenderer.sendToHost('webviewPointerOut') +}) + +window.addEventListener('pointerover', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerOver')") + ipcRenderer.sendToHost('webviewPointerOver') +}) + +window.addEventListener('pointermove', (e) => { + // console.log("ipcRenderer.sendToHost('webviewPointerMove')") + ipcRenderer.sendToHost('webviewPointerMove') +}) + +window.addEventListener('touchmove', (e) => { + // console.log("ipcRenderer.sendToHost('touchmove')") + ipcRenderer.sendToHost('touchMove') +}) + +window.addEventListener('touchstart', (e) => { + pointerCounter++ + // console.log("ipcRenderer.sendToHost('touchstart')") + ipcRenderer.sendToHost('touchStart') +}) + +window.addEventListener('touchend', (e) => { + pointerCounter-- + // console.log("ipcRenderer.sendToHost('touchend')") + ipcRenderer.sendToHost('touchEnd') +}) + +ipcRenderer.on('overlayEvent', function () { + console.log('hello world From Preload') +}) diff --git a/browser/store.js b/browser/store.js new file mode 100644 index 0000000..799ca3c --- /dev/null +++ b/browser/store.js @@ -0,0 +1,47 @@ +const electron = require('electron') +const path = require('path') +const fs = require('fs') + +class Store { + + constructor(opts) { + + // Renderer process has to get `app` module via `remote`, whereas the main process can get it directly + // app.getPath('userData') will return a string of the user's app data directory path. + const userDataPath = (electron.app || electron.remote.app).getPath('userData') + // We'll use the `configName` property to set the file name and path.join to bring it all together as a string + this.path = path.join(userDataPath, opts.configName + '.json') + + this.data = parseDataFile(this.path, opts.defaults) + this.data = Object.assign(opts.defaults, this.data) + } + + // This will just return the property on the `data` object + get(key) { + return this.data[key] + } + + // ...and this will set it + set(key, val) { + this.data[key] = val + // Wait, I thought using the node.js' synchronous APIs was bad form? + // We're not writing a server so there's not nearly the same IO demand on the process + // Also if we used an async API and our app was quit before the asynchronous write had a chance to complete, + // we might lose that data. Note that in a real app, we would try/catch this. + fs.writeFileSync(this.path, JSON.stringify(this.data)) + } +} + +function parseDataFile(filePath, defaults) { + // We'll try/catch it in case the file doesn't exist yet, which will be the case on the first application run. + // `fs.readFileSync` will return a JSON string which we then parse into a Javascript object + try { + return JSON.parse(fs.readFileSync(filePath)) + } catch(error) { + // if there was some kind of error, return the passed in defaults instead. + return defaults + } +} + +// expose the class +module.exports = Store diff --git a/browser/test.js b/browser/test.js new file mode 100644 index 0000000..2434a4a --- /dev/null +++ b/browser/test.js @@ -0,0 +1,113 @@ +let fs = require('fs') +let path = require('path') +let {thumbnail} = require('./utils.js') + +let pairs = [] +let urlMap = new Map() +function isFile(path) { + try { + return !fs.lstatSync(path).isDirectory() + } catch(e) { + if (e.code == 'ENOENT'){ + return false + } else { + return false + } + } +} + +function listPairs(src, dst, skipFiles=true) { + let dir = fs.readdirSync(src) + for(let name of dir) { + let srcPath = src + name + if (isFile(srcPath) && !skipFiles) { + if (srcPath.endsWith('.html')) { + let dstPath = dst + name.replace(/.html/, '.png') + pairs.push([srcPath, dstPath]) + } + } + else { + if (srcPath.endsWith('.')) + continue + let indexPath = srcPath + path.sep + 'index.html' + if (isFile(indexPath)) { + let thumbnailPath = indexPath.replace(/index.html/, 'thumbnail.png') + pairs.push([indexPath, thumbnailPath]) + } + } + } +} + +function capturePage(focusedWindow, dstPath) { + focusedWindow.capturePage( (image) => { + fs.writeFile(dstPath, thumbnail(image), (err) => { + if (err) throw err; + nextLoadTest(focusedWindow) + }) + }) +} + +function testLoading(focusedWindow, filePath, dstPath) { + let urlPath = filePath.replace(path.sep, '/') + let basePath = `${__dirname}`.replace('/browser', '') + let shortURL = `file://${basePath}/${urlPath}` + let fullURL = `file://${__dirname}/../../${urlPath}` + // console.log({basePath, shortURL, fullURL}) + if (focusedWindow) { + urlMap.set(shortURL, dstPath) + focusedWindow.webContents.session.clearCache(() => console.log('Cache cleared')) + focusedWindow.webContents.loadURL(shortURL) + } +} + +function listLibPairs() { + let src = "lib" + path.sep + let dst = "lib" + path.sep + 'thumbnails' + path.sep + listPairs(src, dst, false) +} + +function listAppPairs() { + let src = "apps" + path.sep + let dst = "apps" + path.sep + listPairs(src, dst) +} + +function listSrcPairs() { + let src = "src" + path.sep + let dst = "src" + path.sep + listPairs(src, dst) +} + +function nextLoadTest(focusedWindow) { + if (global.errorCount > 0 && global.stopTestsOnError) { + console.log("Test aborted") + return + } + if (pairs.length > 0) { + let [file, image] = pairs.pop() + testLoading(focusedWindow, file, image) + } + else { + console.log("All thumbnails created") + } +} + +function loadTests(focusedWindow) { + global.errorCount = 0 + focusedWindow.webContents.on('did-finish-load', (e) => { + setTimeout(() => { + let url = e.sender.history[e.sender.history.length-1] + dstPath = urlMap.get(url) + capturePage(focusedWindow, dstPath) + }, 5000) + }) + + listLibPairs() + listSrcPairs() + listAppPairs() + nextLoadTest(focusedWindow) +} + +module.exports = loadTests + + diff --git a/browser/utils.js b/browser/utils.js new file mode 100644 index 0000000..1fe13f7 --- /dev/null +++ b/browser/utils.js @@ -0,0 +1,38 @@ + +function fileURL(src) { + let dir = __dirname.replace(/\\/g, '/') + return `file://${dir}/${src}` +} + +function loadScript(src, callback) { + let url = fileURL(src) + let script = document.createElement('script') + script.onload = () => { + if (callback) { + callback.call(this, script) + } + } + script.src = url + document.head.appendChild(script) +} + +function thumbnail(screenshot) { + return screenshot.resize({ width: 1024 }).toPNG() +} + +let hiddenCursor = fileURL('../assets/cursor/cur0000.cur') +let defaultCursor = fileURL('../assets/cursor/cur1054.cur') + +function hideCursor() { +// console.log("hideCursor") + document.body.style.cursor = `url('${hiddenCursor}'), default` +} + +function showCursor() { + document.body.style.cursor = `url('${defaultCursor}'), default` +// console.log("showCursor") +} + +module.exports = { + fileURL, loadScript, thumbnail, hideCursor, showCursor +} diff --git a/css/doctest.css b/css/doctest.css index 2d00933..fc9573c 100644 --- a/css/doctest.css +++ b/css/doctest.css @@ -18,7 +18,7 @@ html { margin: 0 auto; } -.dark-mode nav{ +.dark-mode nav { border-color: var(--white); } @@ -264,3 +264,23 @@ canvas { width: 100%; height: 100%; } + +#ctxmenu { + position: fixed; + background: white; + color: black; + cursor: pointer; + border: 1px lightgray solid; +} + +#ctxmenu > a { + display: block; + padding: 0.25rem 1rem; + font-size: 18px; + margin: 0.125rem; +} + +#ctxmenu > a:hover { + background: black; + color: white; +} diff --git a/css/index.css b/css/index.css index 10cff7f..3570e22 100644 --- a/css/index.css +++ b/css/index.css @@ -27,6 +27,10 @@ body { -moz-osx-font-smoothing: grayscale; } +h2 { + padding-left: 20px; +} + h3 { color: white; padding: 4px; @@ -34,7 +38,9 @@ h3 { background-color: rgba(0, 0, 15, 0.5); } -a { text-decoration: none; } +a { + text-decoration: none; +} div.wrapper { overflow: hidden; @@ -46,28 +52,48 @@ div.wrapper { /* Color animation from https://www.tjvantoll.com/2012/02/20/css3-color-animations/ */ @-webkit-keyframes color_change { - from { background-color: rgba(0, 0, 0, 0); } - to { background-color: red; } + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: red; + } } @-moz-keyframes color_change { - from { background-color: rgba(0, 0, 0, 0); } - to { background-color: red; } + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: red; + } } @-ms-keyframes color_change { - from { background-color: rgba(0, 0, 0, 0); } - to { background-color: red; } + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: red; + } } @-o-keyframes color_change { - from { background-color: rgba(0, 0, 0, 0); } - to { background-color: red; } + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: red; + } } @keyframes color_change { - from { background-color: rgba(0, 0, 0, 0); } - to { background-color: red; } + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: red; + } } /*** CSS taken from https://medium.com/@jamesfuthey/simulating-the-creation-of-website-thumbnail-screenshots-using-iframes-7145269891db#.7v7fshos5 ***/ @@ -83,7 +109,7 @@ div.wrapper { } .thumbnail::after { - content: ""; + content: ''; display: block; position: absolute; top: 0; @@ -145,12 +171,12 @@ div.title { min-height: 100%; min-width: 100%; display: -webkit-flex; - -webkit-align-items: flex-end; - align-items: flex-end; + -webkit-align-items: flex-start; + align-items: flex-start; -webkit-flex-wrap: wrap; flex-wrap: wrap; - -webkit-align-content: flex-end; - align-content: flex-end; + -webkit-align-content: flex-start; + align-content: flex-start; } /** See https://github.com/electron/electron/issues/4420 */ diff --git a/doc/ast.html b/doc/ast.html index fc06f6d..9c02028 100644 --- a/doc/ast.html +++ b/doc/ast.html @@ -1,30 +1,28 @@ - + @@ -32,104 +30,106 @@ \ No newline at end of file + return data + } + diff --git a/lib/_menu.js b/lib/_menu.js new file mode 100755 index 0000000..8b744d1 --- /dev/null +++ b/lib/_menu.js @@ -0,0 +1,27 @@ +const mapping = { + 'lib.Application': './app.html', + 'lib.Capabilities': './capabilities.html', + 'lib.Card': './card/index.html', + 'lib.Pixi': './pixi/index.html', + 'pixi.App': './pixi/app.html' +} + +function menu(event) { + let key = event.target.innerText + let html = '' + + for (let k of Object.keys(mapping)) { + if (k.startsWith(key)) { + let rest = k.slice(key.length) + let url = mapping[k] + html += `${rest}` + } + } + event.preventDefault() + let contextMenu = document.createElement('div') + contextMenu.id = 'ctxmenu' + contextMenu.style = `top:${event.pageY - 10}px;left:${event.pageX - 40}px` + contextMenu.onmouseleave = () => (contextMenu.outerHTML = '') + contextMenu.innerHTML = html + document.body.appendChild(contextMenu) +} diff --git a/lib/app.html b/lib/app.html index e321874..51f18cc 100644 --- a/lib/app.html +++ b/lib/app.html @@ -3,6 +3,7 @@ + App @@ -12,7 +13,7 @@

- Application + lib.Application

IWM Browser Applications follow a common three phase pattern, shared by many programming environments as diverse as Processing, Arduino, Intern, etc. diff --git a/lib/capabilities.html b/lib/capabilities.html index c98aae1..7a6fbca 100644 --- a/lib/capabilities.html +++ b/lib/capabilities.html @@ -1,75 +1,56 @@ - + - - - Doctests Capabilities - - - - - - -

-

- Capabilities -

-

Browsers differ in many aspects, from touch support, support of CSS and HTML5 standards, to -javascript versions. This page collects some of these differences. -

- User Agent -

-

-

-

- Device Pixel Ratio -

-

-

-

- Multi Touch Table -

-

-

-

- Supported Events -

-

-

- + + + +
+

lib.Capabilities

+

+ Browsers differ in many aspects, from touch support, support of CSS and HTML5 standards, to javascript + versions. This page collects some of these differences. +

- Doctest.expect(Capabilities.supportsMouseEvents(), true) +

User Agent

+

+

Device Pixel Ratio

+

+

Multi Touch Table

+

+

Supported Events

+

+ -

- Interactive Alerts -

-

- Standard alerts are displayed quite differently, on Windows 10, for instance - the browser URL is encluded, and a checkbox that allows to hide the - alert dialogs. -

- - - -

- Result -

-
+ if (Capabilities.supportsPointerEvents()) { + Doctest.expect(Capabilities.supportsPointerEvents(), true) + } + +

Interactive Alerts

+

+ Standard alerts are displayed quite differently, on Windows 10, for instance the browser URL is + encluded, and a checkbox that allows to hide the alert dialogs. +

+ + + +

Result

+
- -

- References -

- -
- +

References

+ +
+ + diff --git a/lib/card/index.html b/lib/card/index.html index aee6dd4..cbc6e6c 100644 --- a/lib/card/index.html +++ b/lib/card/index.html @@ -15,7 +15,7 @@

- Cards + lib.card.Cards

Cards implement a central UI metaphor for multiuser applications. They allow users to explore information spaces diff --git a/lib/coordinates.html b/lib/coordinates.html index bb22d94..04456f3 100644 --- a/lib/coordinates.html +++ b/lib/coordinates.html @@ -1,110 +1,115 @@ - + + + + Coordinates Doctest + + + + + + - - - - + function animate(callback) { + requestAnimationFrame((dt) => { + drawPolygons() + callback() + animate(callback) + }) + } + + - -

- Coordinates -

-

- To position objects in defined spatial relationships presupposes a clear understanding of the involved coordinate systems. - Unfortunately, several systems with several conventions are involved: - DOM & CSS, - SVG, - Canvas -

-

- We need a common reference system to switch between these coordinate systems. As the uttermost context, the browser page - coordinate system is the most natural one. A simple API was long missing but has now been established in most modern - browsers with - window.convertPointFromNoteToPage and the inverse - window.convertPointFromPageToNode. - Although MDN Web Docs warns about their Non-standard nature the methods work in browsers targeted - by the IWM Browser project. This doctest assures that this assumption can be tested. -

-

Let's look at a scatter object with a rotatable local coordinate system. We try to follow a point in this local coordinate - system by showing a marker outside the scatter that follows the point. -

-
- - + - - Canvas not supported. - -
+ + Canvas not supported. + + - - - \ No newline at end of file + context.strokeStyle = 'red' + context.beginPath() + context.arc(canvasPoint.x, canvasPoint.y, 12, 0, Math.PI * 2) + context.stroke() + } + animate(followPoint) + + + diff --git a/lib/doctest.html b/lib/doctest.html index ab5fb7b..6942ec3 100644 --- a/lib/doctest.html +++ b/lib/doctest.html @@ -12,7 +12,7 @@

- Doctests + lib.Doctests

Doctests are explanatory descriptions of programs with executable code examples. diff --git a/lib/electron.html b/lib/electron.html index 90e4235..ef03e26 100644 --- a/lib/electron.html +++ b/lib/electron.html @@ -1,47 +1,48 @@ - + - - - Electron Node.js Test - - - - - - - -

-

- Electron Node.js Test -

-

-This doctest is expected to work only within the IWMBrowser. IWMBrowser windows -are opened with a preload.js which is evaluated before the HTML is loaded. -According to -"I can not use jQuery/RequireJS/Meteor/AngularJS in Electron" we -have to rename the symbols in the page before other libraries are included. -In order to access node.js modules we can use `nodeRequire` instead. -

-

As a simple test we try to load a file from the filesystem:

- -

As simple as this test is, it shows that within the IWMBrowser one import all node.js -modules. Don't forget to test for nodeRequire to avoid runtime errors in other browsers. -

-

- References -

- -
- + + + Electron Node.js Test + + + + + + + +
+

Electron Node.js Test

+

+ This doctest is expected to work only within the IWMBrowser. IWMBrowser windows are opened with a + preload.js which is evaluated before the HTML is loaded. According to + "I can not use jQuery/RequireJS/Meteor/AngularJS in Electron" + we have to rename the symbols in the page before other libraries are included. In order to access + node.js modules we can use `nodeRequire` instead. +

+

As a simple test we try to load a file from the filesystem:

+ +

+ As simple as this test is, it shows that within the IWMBrowser one import all node.js modules. Don't + forget to test for nodeRequire to avoid runtime errors in other browsers. +

+

References

+ +
+ + diff --git a/lib/events.html b/lib/events.html index d17af68..dbb0baf 100644 --- a/lib/events.html +++ b/lib/events.html @@ -1,26 +1,25 @@ - + - - - - - - - - - -

- Events -

-

-For functional tests it can be useful to simulate event or record and playback events. -This module provides basic support for extracting data from events and serializing -events into a JSON format that allows to save and load sequences of events. -

-

-Let's look at an example of a HTML structure with click handlers. The click -handler actions log messages that can be tested.

-

+    
+        
+        
+        
+        
+        
+        
+    
+    
+        

lib.Events

+

+ For functional tests it can be useful to simulate event or record and playback events. This module provides + basic support for extracting data from events and serializing events into a JSON format that allows to save + and load sequences of events. +

+

+ Let's look at an example of a HTML structure with click handlers. The click handler actions log messages + that can be tested. +

+

     <div>
         <img id="women" src="examples/women.jpeg"
             onclick="record(event); Doctest.log('Lady clicked')"/>
@@ -31,46 +30,53 @@ handler actions log messages that can be tested.

</vide> </div>
-
- - -
- - -

- References -

- - - + Doctest.expectLog('Lady clicked', 'Movie clicked') + +

References

+ + + diff --git a/lib/flippable.html b/lib/flippable.html index a4ec6a7..bd45f11 100644 --- a/lib/flippable.html +++ b/lib/flippable.html @@ -1,5 +1,13 @@ + + + Flippable Doctest + + + + + Flippable Doctest @@ -9,6 +17,13 @@ + +