diff --git a/libraries/chrome-tabs/chrome-tabs.css b/libraries/chrome-tabs/chrome-tabs.css new file mode 100644 index 000000000..7c4a51f75 --- /dev/null +++ b/libraries/chrome-tabs/chrome-tabs.css @@ -0,0 +1,175 @@ +.chrome-tabs { + box-sizing: border-box; + position: relative; + height: 33px; + background: var(--main-background-color); + border-radius: 5px 5px 0 0; + overflow: hidden; +} +.chrome-tabs * { + box-sizing: inherit; + font: inherit; +} +.chrome-tabs .chrome-tabs-content { + position: relative; + width: 100%; + height: 100%; +} +.chrome-tabs .chrome-tab { + position: absolute; + left: 0; + height: 36px; + width: 240px; + border: 0; + margin: 0; + z-index: 1; + pointer-events: none; +} + +.chrome-tabs .chrome-tab[active] { + z-index: 5; +} + +.chrome-tabs .chrome-tab, +.chrome-tabs .chrome-tab * { + user-select: none; + cursor: default; +} + +.chrome-tabs .chrome-tab.chrome-tab-was-just-added { + top: 10px; + animation: chrome-tab-was-just-added 120ms forwards ease-in-out; +} +.chrome-tabs .chrome-tab .chrome-tab-content { + position: absolute; + display: flex; + top: 0; + bottom: 0; + left: var(--tab-content-margin); + right: var(--tab-content-margin); + padding: 5px 8px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + overflow: hidden; + pointer-events: all; + background-color: var(--accented-background-color); +} + +.chrome-tabs .chrome-tab[active] .chrome-tab-content { + background-color: var(--more-accented-background-color); +} + +.chrome-tabs .chrome-tab[is-mini] .chrome-tab-content { + padding-left: 2px; + padding-right: 2px; +} +.chrome-tabs .chrome-tab .chrome-tab-title { + flex: 1; + vertical-align: top; + overflow: hidden; + white-space: nowrap; + color: var(--muted-text-color); +} +.chrome-tabs .chrome-tab[is-small] .chrome-tab-title { + margin-left: 0; +} +.chrome-tabs .chrome-tab[active] .chrome-tab-title { + color: var(--main-text-color); +} +.chrome-tabs .chrome-tab .chrome-tab-drag-handle { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} +.chrome-tabs .chrome-tab .chrome-tab-close { + flex-grow: 0; + flex-shrink: 0; + position: relative; + top: 3px; + width: 16px; + height: 16px; + border-radius: 50%; + background-image: url("data:image/svg+xml;utf8,"); + background-position: center center; + background-repeat: no-repeat; + background-size: 8px 8px; +} +@media (hover: hover) { + .chrome-tabs .chrome-tab .chrome-tab-close:hover { + background-color: var(--more-accented-background-color); + } + .chrome-tabs .chrome-tab .chrome-tab-close:hover:active { + background-color: var(--more-accented-background-color); + } +} +@media not all and (hover: hover) { + .chrome-tabs .chrome-tab .chrome-tab-close:active { + background-color: #dadce0; + } +} +@media (hover: hover) { + .chrome-tabs .chrome-tab:not([active]) .chrome-tab-close:not(:hover):not(:active) { + opacity: 0.8; + } +} +.chrome-tabs .chrome-tab[is-smaller] .chrome-tab-close { + margin-left: auto; +} +.chrome-tabs .chrome-tab[is-mini]:not([active]) .chrome-tab-close { + display: none; +} +.chrome-tabs .chrome-tab[is-mini][active] .chrome-tab-close { + margin-left: auto; + margin-right: auto; +} +@-moz-keyframes chrome-tab-was-just-added { + to { + top: 0; + } +} +@-webkit-keyframes chrome-tab-was-just-added { + to { + top: 0; + } +} +@-o-keyframes chrome-tab-was-just-added { + to { + top: 0; + } +} +@keyframes chrome-tab-was-just-added { + to { + top: 0; + } +} +.chrome-tabs.chrome-tabs-is-sorting .chrome-tab:not(.chrome-tab-is-dragging), +.chrome-tabs:not(.chrome-tabs-is-sorting) .chrome-tab.chrome-tab-was-just-dragged { + transition: transform 120ms ease-in-out; +} +.chrome-tabs .chrome-tabs-bottom-bar { + position: absolute; + bottom: 0; + height: 4px; + left: 0; + width: 100%; + background: #fff; + z-index: 10; +} +.chrome-tabs-optional-shadow-below-bottom-bar { + position: relative; + height: 1px; + width: 100%; + background-image: url("data:image/svg+xml;utf8,"); + background-size: 1px 1px; + background-repeat: repeat-x; + background-position: 0% 0%; +} +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { + .chrome-tabs-optional-shadow-below-bottom-bar { + background-image: url("data:image/svg+xml;utf8,"); + } +} diff --git a/libraries/chrome-tabs/chrome-tabs.js b/libraries/chrome-tabs/chrome-tabs.js new file mode 100644 index 000000000..3de62e123 --- /dev/null +++ b/libraries/chrome-tabs/chrome-tabs.js @@ -0,0 +1,369 @@ +/*! + * Draggabilly PACKAGED v2.2.0 + * Make that shiz draggable + * https://draggabilly.desandro.com + * MIT license + */ + +!function(i,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(t){return e(i,t)}):"object"==typeof module&&module.exports?module.exports=e(i,require("jquery")):i.jQueryBridget=e(i,i.jQuery)}(window,function(t,i){"use strict";var c=Array.prototype.slice,e=t.console,p=void 0===e?function(){}:function(t){e.error(t)};function n(d,o,u){(u=u||i||t.jQuery)&&(o.prototype.option||(o.prototype.option=function(t){u.isPlainObject(t)&&(this.options=u.extend(!0,this.options,t))}),u.fn[d]=function(t){if("string"==typeof t){var i=c.call(arguments,1);return s=i,a="$()."+d+'("'+(r=t)+'")',(e=this).each(function(t,i){var e=u.data(i,d);if(e){var n=e[r];if(n&&"_"!=r.charAt(0)){var o=n.apply(e,s);h=void 0===h?o:h}else p(a+" is not a valid method")}else p(d+" not initialized. Cannot call methods, i.e. "+a)}),void 0!==h?h:e}var e,r,s,h,a,n;return n=t,this.each(function(t,i){var e=u.data(i,d);e?(e.option(n),e._init()):(e=new o(i,n),u.data(i,d,e))}),this},r(u))}function r(t){!t||t&&t.bridget||(t.bridget=n)}return r(i||t.jQuery),n}),function(t,i){"use strict";"function"==typeof define&&define.amd?define("get-size/get-size",[],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():t.getSize=i()}(window,function(){"use strict";function m(t){var i=parseFloat(t);return-1==t.indexOf("%")&&!isNaN(i)&&i}var e="undefined"==typeof console?function(){}:function(t){console.error(t)},y=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],b=y.length;function E(t){var i=getComputedStyle(t);return i||e("Style returned "+i+". Are you running this code in a hidden iframe on Firefox? See http://bit.ly/getsizebug1"),i}var _,x=!1;function P(t){if(function(){if(!x){x=!0;var t=document.createElement("div");t.style.width="200px",t.style.padding="1px 2px 3px 4px",t.style.borderStyle="solid",t.style.borderWidth="1px 2px 3px 4px",t.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(t);var e=E(t);P.isBoxSizeOuter=_=200==m(e.width),i.removeChild(t)}}(),"string"==typeof t&&(t=document.querySelector(t)),t&&"object"==typeof t&&t.nodeType){var i=E(t);if("none"==i.display)return function(){for(var t={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},i=0;i {} + + const closest = (value, array) => { + let closest = Infinity + let closestIndex = -1 + + array.forEach((v, i) => { + if (Math.abs(value - v) < closest) { + closest = Math.abs(value - v) + closestIndex = i + } + }) + + return closestIndex + } + + const tabTemplate = ` +
+
+
+ +
+
+
+
+
+
+
+
+ ` + + const defaultTapProperties = { + title: 'New tab', + favicon: false + } + + let instanceId = 0 + + class ChromeTabs { + constructor() { + this.draggabillies = [] + } + + init(el) { + this.el = el + + this.instanceId = instanceId + this.el.setAttribute('data-chrome-tabs-instance-id', this.instanceId) + instanceId += 1 + + this.setupCustomProperties() + this.setupStyleEl() + this.setupEvents() + this.layoutTabs() + this.setupDraggabilly() + } + + emit(eventName, data) { + this.el.dispatchEvent(new CustomEvent(eventName, { detail: data })) + } + + setupCustomProperties() { + this.el.style.setProperty('--tab-content-margin', `${ TAB_CONTENT_MARGIN }px`) + } + + setupStyleEl() { + this.styleEl = document.createElement('style') + this.el.appendChild(this.styleEl) + } + + setupEvents() { + window.addEventListener('resize', _ => { + this.cleanUpPreviouslyDraggedTabs() + this.layoutTabs() + }) + + this.el.addEventListener('dblclick', event => { + if ([this.el, this.tabContentEl].includes(event.target)) this.addTab() + }) + + this.tabEls.forEach((tabEl) => this.setTabCloseEventListener(tabEl)) + } + + get tabEls() { + return Array.prototype.slice.call(this.el.querySelectorAll('.chrome-tab')) + } + + get tabContentEl() { + return this.el.querySelector('.chrome-tabs-content') + } + + get tabContentWidths() { + const numberOfTabs = this.tabEls.length + const tabsContentWidth = this.tabContentEl.clientWidth + const tabsCumulativeOverlappedWidth = (numberOfTabs - 1) * TAB_CONTENT_OVERLAP_DISTANCE + const targetWidth = (tabsContentWidth - (2 * TAB_CONTENT_MARGIN) + tabsCumulativeOverlappedWidth) / numberOfTabs + const clampedTargetWidth = Math.max(TAB_CONTENT_MIN_WIDTH, Math.min(TAB_CONTENT_MAX_WIDTH, targetWidth)) + const flooredClampedTargetWidth = Math.floor(clampedTargetWidth) + const totalTabsWidthUsingTarget = (flooredClampedTargetWidth * numberOfTabs) + (2 * TAB_CONTENT_MARGIN) - tabsCumulativeOverlappedWidth + const totalExtraWidthDueToFlooring = tabsContentWidth - totalTabsWidthUsingTarget + + // TODO - Support tabs with different widths / e.g. "pinned" tabs + const widths = [] + let extraWidthRemaining = totalExtraWidthDueToFlooring + for (let i = 0; i < numberOfTabs; i += 1) { + const extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0 + widths.push(flooredClampedTargetWidth + extraWidth) + if (extraWidthRemaining > 0) extraWidthRemaining -= 1 + } + + return widths + } + + get tabContentPositions() { + const positions = [] + const tabContentWidths = this.tabContentWidths + + let position = TAB_CONTENT_MARGIN + tabContentWidths.forEach((width, i) => { + const offset = i * TAB_CONTENT_OVERLAP_DISTANCE + positions.push(position - offset) + position += width + }) + + return positions + } + + get tabPositions() { + const positions = [] + + this.tabContentPositions.forEach((contentPosition) => { + positions.push(contentPosition - TAB_CONTENT_MARGIN) + }) + + return positions + } + + layoutTabs() { + const tabContentWidths = this.tabContentWidths + + this.tabEls.forEach((tabEl, i) => { + const contentWidth = tabContentWidths[i] + const width = contentWidth + (2 * TAB_CONTENT_MARGIN) + + tabEl.style.width = width + 'px' + tabEl.removeAttribute('is-small') + tabEl.removeAttribute('is-smaller') + tabEl.removeAttribute('is-mini') + + if (contentWidth < TAB_SIZE_SMALL) tabEl.setAttribute('is-small', '') + if (contentWidth < TAB_SIZE_SMALLER) tabEl.setAttribute('is-smaller', '') + if (contentWidth < TAB_SIZE_MINI) tabEl.setAttribute('is-mini', '') + }) + + let styleHTML = '' + this.tabPositions.forEach((position, i) => { + styleHTML += ` + .chrome-tabs[data-chrome-tabs-instance-id="${ this.instanceId }"] .chrome-tab:nth-child(${ i + 1 }) { + transform: translate3d(${ position }px, 0, 0) + } + ` + }) + this.styleEl.innerHTML = styleHTML + } + + createNewTabEl() { + const div = document.createElement('div') + div.innerHTML = tabTemplate + return div.firstElementChild + } + + addTab(tabProperties, { animate = true, background = false } = {}) { + const tabEl = this.createNewTabEl() + + if (animate) { + tabEl.classList.add('chrome-tab-was-just-added') + setTimeout(() => tabEl.classList.remove('chrome-tab-was-just-added'), 500) + } + + tabProperties = Object.assign({}, defaultTapProperties, tabProperties) + this.tabContentEl.appendChild(tabEl) + this.setTabCloseEventListener(tabEl) + this.updateTab(tabEl, tabProperties) + this.emit('tabAdd', { tabEl }) + if (!background) this.setCurrentTab(tabEl) + this.cleanUpPreviouslyDraggedTabs() + this.layoutTabs() + this.setupDraggabilly() + } + + setTabCloseEventListener(tabEl) { + tabEl.querySelector('.chrome-tab-close').addEventListener('click', _ => this.removeTab(tabEl)) + } + + get activeTabEl() { + return this.el.querySelector('.chrome-tab[active]') + } + + hasActiveTab() { + return !!this.activeTabEl + } + + setCurrentTab(tabEl) { + const activeTabEl = this.activeTabEl + if (activeTabEl === tabEl) return + if (activeTabEl) activeTabEl.removeAttribute('active') + tabEl.setAttribute('active', '') + this.emit('activeTabChange', { tabEl }) + } + + removeTab(tabEl) { + if (tabEl === this.activeTabEl) { + if (tabEl.nextElementSibling) { + this.setCurrentTab(tabEl.nextElementSibling) + } else if (tabEl.previousElementSibling) { + this.setCurrentTab(tabEl.previousElementSibling) + } + } + tabEl.parentNode.removeChild(tabEl) + this.emit('tabRemove', { tabEl }) + this.cleanUpPreviouslyDraggedTabs() + this.layoutTabs() + this.setupDraggabilly() + } + + updateTab(tabEl, tabProperties) { + tabEl.querySelector('.chrome-tab-title').textContent = tabProperties.title + + const faviconEl = tabEl.querySelector('.chrome-tab-favicon') + if (tabProperties.favicon) { + faviconEl.style.backgroundImage = `url('${ tabProperties.favicon }')` + faviconEl.removeAttribute('hidden', '') + } else { + faviconEl.setAttribute('hidden', '') + faviconEl.removeAttribute('style') + } + + if (tabProperties.id) { + tabEl.setAttribute('data-tab-id', tabProperties.id) + } + } + + cleanUpPreviouslyDraggedTabs() { + this.tabEls.forEach((tabEl) => tabEl.classList.remove('chrome-tab-was-just-dragged')) + } + + setupDraggabilly() { + const tabEls = this.tabEls + const tabPositions = this.tabPositions + + if (this.isDragging) { + this.isDragging = false + this.el.classList.remove('chrome-tabs-is-sorting') + this.draggabillyDragging.element.classList.remove('chrome-tab-is-dragging') + this.draggabillyDragging.element.style.transform = '' + this.draggabillyDragging.dragEnd() + this.draggabillyDragging.isDragging = false + this.draggabillyDragging.positionDrag = noop // Prevent Draggabilly from updating tabEl.style.transform in later frames + this.draggabillyDragging.destroy() + this.draggabillyDragging = null + } + + this.draggabillies.forEach(d => d.destroy()) + + tabEls.forEach((tabEl, originalIndex) => { + const originalTabPositionX = tabPositions[originalIndex] + const draggabilly = new Draggabilly(tabEl, { + axis: 'x', + handle: '.chrome-tab-drag-handle', + containment: this.tabContentEl + }) + + this.draggabillies.push(draggabilly) + + draggabilly.on('pointerDown', _ => { + this.setCurrentTab(tabEl) + }) + + draggabilly.on('dragStart', _ => { + this.isDragging = true + this.draggabillyDragging = draggabilly + tabEl.classList.add('chrome-tab-is-dragging') + this.el.classList.add('chrome-tabs-is-sorting') + }) + + draggabilly.on('dragEnd', _ => { + this.isDragging = false + const finalTranslateX = parseFloat(tabEl.style.left, 10) + tabEl.style.transform = `translate3d(0, 0, 0)` + + // Animate dragged tab back into its place + requestAnimationFrame(_ => { + tabEl.style.left = '0' + tabEl.style.transform = `translate3d(${ finalTranslateX }px, 0, 0)` + + requestAnimationFrame(_ => { + tabEl.classList.remove('chrome-tab-is-dragging') + this.el.classList.remove('chrome-tabs-is-sorting') + + tabEl.classList.add('chrome-tab-was-just-dragged') + + requestAnimationFrame(_ => { + tabEl.style.transform = '' + + this.layoutTabs() + this.setupDraggabilly() + }) + }) + }) + }) + + draggabilly.on('dragMove', (event, pointer, moveVector) => { + // Current index be computed within the event since it can change during the dragMove + const tabEls = this.tabEls + const currentIndex = tabEls.indexOf(tabEl) + + const currentTabPositionX = originalTabPositionX + moveVector.x + const destinationIndexTarget = closest(currentTabPositionX, tabPositions) + const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget)) + + if (currentIndex !== destinationIndex) { + this.animateTabMove(tabEl, currentIndex, destinationIndex) + } + }) + }) + } + + animateTabMove(tabEl, originIndex, destinationIndex) { + if (destinationIndex < originIndex) { + tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex]) + } else { + tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex + 1]) + } + this.emit('tabReorder', { tabEl, originIndex, destinationIndex }) + this.layoutTabs() + } + } + + if (isNodeContext) { + module.exports = ChromeTabs + } else { + window.ChromeTabs = ChromeTabs + } +})() diff --git a/src/public/javascripts/desktop.js b/src/public/javascripts/desktop.js index 2f8688f8b..b333e9385 100644 --- a/src/public/javascripts/desktop.js +++ b/src/public/javascripts/desktop.js @@ -156,4 +156,14 @@ noteTypeService.init(); linkService.init(); -noteAutocompleteService.init(); \ No newline at end of file +noteAutocompleteService.init(); + +$(document).ready(() => { + const el = $('.chrome-tabs')[0]; + const chromeTabs = new ChromeTabs(); + chromeTabs.init(el); + + el.addEventListener('activeTabChange', ({detail}) => console.log('Active tab changed', detail.tabEl)); + el.addEventListener('tabAdd', ({detail}) => console.log('Tab added', detail.tabEl)); + el.addEventListener('tabRemove', ({detail}) => console.log('Tab removed', detail.tabEl)); +}); \ No newline at end of file diff --git a/src/public/stylesheets/desktop.css b/src/public/stylesheets/desktop.css index f48bdc896..67e49913a 100644 --- a/src/public/stylesheets/desktop.css +++ b/src/public/stylesheets/desktop.css @@ -8,10 +8,12 @@ body { display: grid; grid-template-areas: "header header" + "left-pane tabs" "left-pane title" "left-pane note-detail"; grid-template-rows: auto auto + auto 1fr; justify-content: center; diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 966850cf1..17e0fabcf 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -231,6 +231,10 @@ button.close { grid-area: title; } +#tabs-container { + grid-area: tabs; +} + #note-title { margin-left: 15px; margin-right: 10px; diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index 1442140b3..e809f3809 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -150,6 +150,41 @@ +
+
+
+
+
+
Google
+
+
+
+
+
+
+
Facebook
+
+
+
+
+
+ + +
+
+