diff --git a/viewer/css/main.css b/viewer/css/main.css index abf36af1f..29ddbce38 100644 --- a/viewer/css/main.css +++ b/viewer/css/main.css @@ -97,6 +97,31 @@ body, html { overflow: hidden; background: url("../images/noisy_grid.png") #fff; } +.cmv-widget { + position:absolute; + z-index:40; +} +.cmv-widget-mapinfo { + bottom: 0; + left: 0; +} +.cmv-widget-basemaps { + right: 20px; + top: 20px; +} +.cmv-widget-growler { + right: 20px; + top: 60px; + width: 250px; +} +.cmv-widgets-left { + left: 19px; + top: 87px; +} +.cmv-widget-geocoder { + left: 64px; + top: 20px; +} .panetop { border-top: 1px solid #B5BCC7; } diff --git a/viewer/js/config/app.js b/viewer/js/config/app.js index f83155e22..87061e9bb 100644 --- a/viewer/js/config/app.js +++ b/viewer/js/config/app.js @@ -34,9 +34,12 @@ 'viewer/_ConfigMixin', // manage the Configuration 'viewer/_LayoutMixin', // build and manage the Page Layout and User Interface 'viewer/_MapMixin', // build and manage the Map - 'viewer/_WidgetsMixin' // build and manage the Widgets + 'viewer/_WidgetsMixin', // build and manage the Widgets // 'viewer/_WebMapMixin' // for WebMaps + + 'viewer/_SidebarMixin' // for mobile sidebar + //'config/_customMixin' ], function ( @@ -46,9 +49,11 @@ _ConfigMixin, _LayoutMixin, _MapMixin, - _WidgetsMixin + _WidgetsMixin, // _WebMapMixin + + _SidebarMixin //_MyCustomMixin ) { @@ -61,6 +66,10 @@ // for the most part they are interchangeable, except _ConfigMixin // and _ControllerBase // + + // Mixin for Mobile Sidebar + _SidebarMixin, + _LayoutMixin, _WidgetsMixin, // _WebMapMixin, diff --git a/viewer/js/config/nls/es/main.js b/viewer/js/config/nls/es/main.js index 47acf6117..fee18694a 100644 --- a/viewer/js/config/nls/es/main.js +++ b/viewer/js/config/nls/es/main.js @@ -44,6 +44,7 @@ define({ legend: 'Leyenda', locale: 'Lugar', print: 'Imprimir', + search: 'Buscar', streetview: 'Google Street View' } } diff --git a/viewer/js/config/nls/fr/main.js b/viewer/js/config/nls/fr/main.js index dbf7b877d..b95757dba 100644 --- a/viewer/js/config/nls/fr/main.js +++ b/viewer/js/config/nls/fr/main.js @@ -44,6 +44,7 @@ define({ legend: 'Légende', locale: 'Lieu', print: 'Impression', + search: 'Rechercher', streetview: 'Google StreetView' } } diff --git a/viewer/js/config/nls/main.js b/viewer/js/config/nls/main.js index 2a9a8a606..a1ca815cd 100644 --- a/viewer/js/config/nls/main.js +++ b/viewer/js/config/nls/main.js @@ -46,6 +46,7 @@ define({ legend: 'Legend', locale: 'Locale', print: 'Print', + search: 'Search', streetview: 'Google Street View' } } diff --git a/viewer/js/config/nls/pt-br/main.js b/viewer/js/config/nls/pt-br/main.js index 3bb82f665..818970576 100644 --- a/viewer/js/config/nls/pt-br/main.js +++ b/viewer/js/config/nls/pt-br/main.js @@ -44,6 +44,7 @@ define({ legend: 'Legendas', locale: 'Localidade', print: 'Imprimir', + search: 'Pesquisar', streetview: 'Google Street View' } } diff --git a/viewer/js/config/nls/pt-pt/main.js b/viewer/js/config/nls/pt-pt/main.js index 3bb82f665..818970576 100644 --- a/viewer/js/config/nls/pt-pt/main.js +++ b/viewer/js/config/nls/pt-pt/main.js @@ -44,6 +44,7 @@ define({ legend: 'Legendas', locale: 'Localidade', print: 'Imprimir', + search: 'Pesquisar', streetview: 'Google Street View' } } diff --git a/viewer/js/config/viewer.js b/viewer/js/config/viewer.js index e3511b6ff..1b97e572c 100644 --- a/viewer/js/config/viewer.js +++ b/viewer/js/config/viewer.js @@ -7,8 +7,9 @@ define([ 'esri/layers/ImageParameters', 'gis/plugins/Google', 'dojo/i18n!./nls/main', - 'dojo/topic' -], function (units, Extent, esriConfig, /*urlUtils,*/ GeometryService, ImageParameters, GoogleMapsLoader, i18n, topic) { + 'dojo/topic', + 'dojo/sniff' +], function (units, Extent, esriConfig, /*urlUtils,*/ GeometryService, ImageParameters, GoogleMapsLoader, i18n, topic, has) { // url to your proxy page, must be on same machine hosting you app. See proxy folder for readme. esriConfig.defaults.io.proxyUrl = 'proxy/proxy.ashx'; @@ -120,6 +121,18 @@ define([ pageTitle: i18n.viewer.titles.pageTitle }, + layout: { + /* possible options for sidebar layout: + true - always use mobile sidebar, false - never use mobile sidebar, + 'mobile' - use sidebar for phones and tablets, 'phone' - use sidebar for phones, + 'touch' - use sidebar for all touch devices, 'tablet' - use sidebar for tablets only (not sure why you'd do this?), + other feature detection supported by dojo/sniff and dojo/has- http://dojotoolkit.org/reference-guide/1.10/dojo/sniff.html + + default value is 'phone' + */ + //sidebar: 'phone' + }, + // user-defined layer types /* layerTypes: { @@ -306,15 +319,18 @@ define([ }, search: { include: true, - type: 'domNode', + type: has('phone') ? 'titlePane' : 'domNode', path: 'esri/dijit/Search', srcNodeRef: 'geocoderButton', + title: i18n.viewer.widgets.search, + iconClass: 'fa-search', + position: 0, options: { map: true, visible: true, enableInfoWindow: false, - enableButtonMode: true, - expanded: false + enableButtonMode: has('phone') ? false : true, + expanded: has('phone') ? true : false } }, basemaps: { @@ -384,7 +400,7 @@ define([ } }, overviewMap: { - include: true, + include: has('phone') ? false : true, id: 'overviewMap', type: 'map', path: 'esri/dijit/OverviewMap', @@ -564,7 +580,7 @@ define([ } }, editor: { - include: true, + include: has('phone') ? false : true, id: 'editor', type: 'titlePane', path: 'gis/dijit/Editor', @@ -617,20 +633,19 @@ define([ }, locale: { include: true, + type: has('phone') ? 'titlePane' : 'domNode', id: 'locale', - //type: 'titlePane', - //position: 0, - //open: true, - type: 'domNode', + position: 0, srcNodeRef: 'geocodeDijit', path: 'gis/dijit/Locale', title: i18n.viewer.widgets.locale, + iconClass: 'fa-flag', options: { - style: 'margin-left: 30px;' + style: has('phone') ? null : 'margin-left: 30px;' } }, help: { - include: true, + include: has('phone') ? false : true, id: 'help', type: 'floating', path: 'gis/dijit/Help', diff --git a/viewer/js/viewer/_LayoutMixin.js b/viewer/js/viewer/_LayoutMixin.js index 4940098d1..8d857c58e 100644 --- a/viewer/js/viewer/_LayoutMixin.js +++ b/viewer/js/viewer/_LayoutMixin.js @@ -63,6 +63,12 @@ define([ } }, collapseButtons: {}, + + loadConfig: function () { + this.detectTouchDevices(); + return this.inherited(arguments); + }, + postConfig: function () { this.layoutDeferred = new Deferred(); return this.inherited(arguments); @@ -73,7 +79,7 @@ define([ this.addTopics(); this.addTitles(); - this.detectTouchDevices(); + this.setPhoneInfoWindow(); this.initPanes(); this.mapDeferred.then(lang.hitch(this, 'createPanes')); @@ -321,7 +327,6 @@ define([ this.positionSideBarToggle(id); }, - // simple feature detection. kinda like dojox/mobile without the overhead detectTouchDevices: function () { if (has('touch') && (has('ios') || has('android') || has('bb'))) { has.add('mobile', true); @@ -330,11 +335,13 @@ define([ } else { has.add('tablet', true); } + } + }, - // use the mobile popup for phones - if (has('phone') && !this.config.mapOptions.infoWindow) { - this.config.mapOptions.infoWindow = new PopupMobile(null, put('div')); - } + setPhoneInfoWindow: function () { + // use the mobile popup for phones + if (has('phone') && !this.config.mapOptions.infoWindow) { + this.config.mapOptions.infoWindow = new PopupMobile(null, put('div')); } } diff --git a/viewer/js/viewer/_SidebarMixin.js b/viewer/js/viewer/_SidebarMixin.js new file mode 100644 index 000000000..db65fe722 --- /dev/null +++ b/viewer/js/viewer/_SidebarMixin.js @@ -0,0 +1,129 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/lang', + 'dojo/dom', + 'dojo/sniff', + 'dojo/Deferred', + 'module', + + 'put-selector' + +], function ( + declare, + lang, + dom, + has, + Deferred, + module, + + put, + + Sidebar +) { + + return declare(null, { + + postConfig: function () { + this.config.layout = this.config.layout || {}; + this._checkForSidebarLayout(); + + if (this.config.layout.sidebar) { + this.inherited(arguments); + this.config.panes = this.mixinDeep(this.config.panes || {}, { + left: { + collapsible: false, + style: 'display:none !important' + } + }); + var deferred = new Deferred(); + require([ + module.uri.substring(0, module.uri.lastIndexOf('/')) + '/sidebar/Sidebar.js' + ], lang.hitch(this, function (sidebar) { + Sidebar = sidebar; + this.mapDeferred.then(lang.hitch(this, '_createSidebar')); + deferred.resolve(); + })); + return deferred; + } + return this.inherited(arguments); + }, + + _checkForSidebarLayout: function () { + var sidebar = this.config.layout.sidebar; + + switch (sidebar) { + // all devices + case true: + break; + + // no devices + case false: + break; + + // tablets and phones + case 'mobile': + if (has('mobile')) { + sidebar = true; + } + break; + + // phones + case 'phone': + if (has('phone')) { + sidebar = true; + } + break; + default: + // perhaps they've configured something we don't expect + if (typeof(sidebar) === 'string') { + if (has(sidebar)) { + sidebar = true; + } + // default is just for phones + } else if (has('phone')) { + sidebar = true; + } + break; + } + this.config.layout.sidebar = sidebar; + }, + + _createSidebar: function () { + var mapContainer = dom.byId(this.map.id); + //create controls div + var mapControlsNode = put(this.map.root, 'div.sidebar-map'); + //move the slider into the controls div + put(mapControlsNode, '>', this.map._slider); + //create sidebar + this.sidebar = new Sidebar({ + map: this.map, + mapContainer: mapContainer, + collapseSyncNode: mapControlsNode + }, put(this.map.root, 'div')); + this.sidebar.startup(); + + this._createTitlePaneWidget = this._createTabPaneWidget; + }, + + _createTabPaneWidget: function (parentId, widgetConfig) { + var tabOptions = widgetConfig.tabOptions || { + id: parentId, + title: widgetConfig.title, + iconClass: widgetConfig.iconClass + }; + + var tab = this.sidebar.createTab(tabOptions); + tab.contentNode = put(tab.containerNode, 'div.sidebar-widget div.sidebar-widget-content'); + + var node = put(tab.contentNode, 'div'); + widgetConfig.type = 'domNode'; + widgetConfig.srcNodeRef = node; + this.createWidgets([ + { + options: widgetConfig + } + ]); + } + + }); +}); \ No newline at end of file diff --git a/viewer/js/viewer/sidebar/Sidebar.js b/viewer/js/viewer/sidebar/Sidebar.js new file mode 100644 index 000000000..7df98b78d --- /dev/null +++ b/viewer/js/viewer/sidebar/Sidebar.js @@ -0,0 +1,204 @@ +define([ + 'dojo/_base/declare', + 'dijit/_WidgetBase', + 'dijit/_TemplatedMixin', + + 'dojo/_base/lang', + 'dojo/_base/array', + 'dojo/query', + 'dojo/dom-class', + 'dojo/dom-geometry', + 'dojo/on', + 'dojo/aspect', + + 'dijit/registry', + + 'put-selector/put', + + 'dojo/text!./templates/Sidebar.html', + + 'xstyle/css!./css/Sidebar.css', + + 'dojo/NodeList-traverse' + +], function ( + declare, + _WidgetBase, + _TemplatedMixin, + + lang, + array, + query, + domClass, + domGeom, + on, + aspect, + + registry, + + put, + + template +) { + return declare([_WidgetBase, _TemplatedMixin], { + templateString: template, + baseClass: 'sidebar', + + defaultTabParams: { + title: 'Title', + iconClass: 'fa-bars' + }, + + viewPadding: { + top: 0, + left: 0, + right: 0, + bottom: 0 + }, + + showCloseIcon: true, + + collapseSyncNode: null, + + postCreate: function () { + this.inherited(arguments); + + this.tabs = []; + if (this.collapseSyncNode) { + if (domClass.contains(this.domNode, 'collapsed')) { + put(this.mapContainer, '.sidebar-collapsed'); + } + //wire up css transition callback covering all event name bases + on(this.collapseSyncNode, 'transitionend, oTransitionEnd, webkitTransitionEnd, animationend, webkitAnimationEnd', lang.hitch(this, '_setViewPadding')); + } + aspect.before(this.map, 'setExtent', lang.hitch(this, '_viewPaddingHandler')); + + // resize tab and any widgets within the tab when it is opened + on(this.domNode, 'transitionend, oTransitionEnd, webkitTransitionEnd, animationend, webkitAnimationEnd', lang.hitch(this, '_resizeActiveTab')); + + + // resize tab and any widgets within the tab when the browser is resized + on(window, 'resize', lang.hitch(this, function () { + window.setTimeout(lang.hitch(this, '_resizeActiveTab'), 300); // 300ms to wait for the animation to complete + })); + + }, + + createTab: function (options) { + options = lang.mixin(lang.clone(this.defaultTabParams), options || {}); + var tab = { + id: options.id, + buttonNode: null, + containerNode: null, + titleNode: null, + closeBtnNode: null, + contentNode: null + }; + //create and place dom elements for the tab button and pane + tab.buttonNode = put(this.tabsButtonNode, 'li a[role=tab] i.fa.' + options.iconClass + '<<'); + tab.containerNode = put(this.tabsContainerNode, 'div.' + this.baseClass + '-pane'); + tab.titleNode = put(tab.containerNode, 'div.' + this.baseClass + '-pane-title $', options.title); + + if (this.showCloseIcon) { + tab.closeBtnNode = put(tab.titleNode, 'i.fa.fa-chevron-left.' + this.baseClass + '-closeIcon'); + // listen for the tab close button click + on(tab.closeBtnNode, 'click', lang.hitch(this, 'tabClickHandler', tab)); + } + + // listen for the tab button click + on(tab.buttonNode, 'click', lang.hitch(this, 'tabClickHandler', tab)); + + //keep a reference to this tab + this.tabs.push(tab); + + //return the tabs pane node + return tab; + }, + + openTab: function (tab) { + array.forEach(this.tabs, function (childTab) { + put(childTab.buttonNode, '!active'); + put(childTab.containerNode, '!active'); + put(childTab.contentNode, '!active'); + }); + put(tab.buttonNode, '.active'); + put(tab.containerNode, '.active'); + put(tab.contentNode, '.active'); + put(this.tabsButtonNode, '.active'); + put(this.domNode, '!collapsed'); + put(this.mapContainer, '!sidebar-collapsed'); + }, + + closeTab: function () { + array.forEach(this.tabs, function (tab) { + put(tab.buttonNode, '!active'); + put(tab.containerNode, '!active'); + put(tab.contentNode, '!active'); + }, this); + put(this.tabsButtonNode, '!active'); + put(this.domNode, '.collapsed'); + put(this.mapContainer, '.sidebar-collapsed'); + }, + + tabClickHandler: function (tab) { + if (domClass.contains(tab.buttonNode, 'active')) { + this.closeTab(tab); + } else { + this.openTab(tab); + } + }, + + _setViewPadding: function () { + var dims = domGeom.getContentBox(this.domNode); + this.viewPadding = { + top: 0, + left: dims.w + dims.l, + right: 0, + bottom: 0 + }; + this._viewPaddingHandler(this.map.extent); + }, + + _viewPaddingHandler: function (extent) { + var map = this.map, + vp = this.viewPadding, + w = map.width - vp.left - vp.right, + h = map.height - vp.top - vp.bottom, + res = Math.max(extent.getWidth() / w, extent.getHeight() / h), + center = extent.getCenter(), + result = map.extent.expand(res / (map.extent.getWidth() / map.width)); + result = result.centerAt({ + x: center.x - (vp.left - vp.right) * 0.5 * res, + y: center.y - (vp.top - vp.bottom) * 0.5 * res + }); + return [result]; + }, + + _resizeActiveTab: function () { + var childTabs = array.filter(this.tabs, function (tab) { + return domClass.contains(tab.contentNode, 'active'); + }); + if (childTabs && childTabs.length > 0) { + var contentNode = query(childTabs[0].contentNode); + this._resizeWidgetsInNodeList(contentNode); + var children = contentNode.children(); + this._resizeWidgetsInNodeList(children); + } + }, + + _resizeWidgetsInNodeList: function (nodes) { + array.forEach(nodes, function (node) { + // resize any widgets + var childWidgets = registry.findWidgets(node); + array.forEach(childWidgets, function (widget) { + if (widget.resize && typeof(widget.resize) === 'function') { + window.setTimeout(function () { + widget.resize(); + }, 50); + } + }); + + }); + } + }); +}); \ No newline at end of file diff --git a/viewer/js/viewer/sidebar/css/Sidebar.css b/viewer/js/viewer/sidebar/css/Sidebar.css new file mode 100644 index 000000000..c1ee89cea --- /dev/null +++ b/viewer/js/viewer/sidebar/css/Sidebar.css @@ -0,0 +1,308 @@ +.cmv .sidebar { + bottom: 0; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); + height: auto; + left: 0; + overflow: hidden; + position: absolute; + top: 0; + width: 100%; + z-index: 40; +} + +.cmv .sidebar.collapsed { + height: auto; + width: 40px; +} + +.cmv .sidebar-tabs { + background-color: #fff; + color: #444; + height: 100%; + margin: 0; + overflow-y: auto; + padding: 0; + position: absolute; + top: 0; + width: 40px; + z-index: 41; +} + +.cmv .sidebar-tabs.active { + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); +} + +.cmv .sidebar-tabs > li { + cursor: pointer; + float: none; + font-size: 14pt; + height: 40px; + overflow: hidden; + transition: all 80ms; + width: 100%; +} + +.cmv .sidebar-tabs > li:hover { + background-color: #444; + color: #fff; +} + +.cmv .sidebar-tabs > li.active { + background-color: #444; + color: #fff; +} + +.cmv .sidebar-tabs > li > a { + color: inherit; + display: block; + height: 100%; + line-height: 40px; + text-align: center; + text-decoration: none; + width: 100%; +} + +.cmv .sidebar-content { + background-color: #fff; + bottom: 0; + left: 40px; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + right: 0; + top: 0; +} + +.cmv .sidebar-pane { + display: none; + right: 0; + width: 100%; +} + +.cmv .sidebar-pane.active { + display: block; +} + +.cmv .sidebar-pane-title { + background-color: #eee; + color: #333; + border-bottom: 1px solid #333; + font-size: 16px; + font-weight: bold; + height: 28px; + left: 0; + padding: 12px 10px 0 10px; + position: absolute; + right: 0; + top: 0; +} + +.cmv .sidebar .sidebar-pane-title .sidebar-closeIcon { + cursor: pointer; + float: right; +} + +.cmv .sidebar-widget { + bottom: 0; + left: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 10px; + position: absolute; + right: 0; + top: 40px; +} + +.cmv .sidebar-widget .sidebar-widget-content { + padding: 5px; + position: relative; +} + + +/* esri map widgets */ +.cmv .map .sidebar-map { + left: 40px; + position: absolute; + top: 0; +} + +.cmv .map .cmv-widget-geocoder { + top: 15px; +} + +.cmv .sidebar, +.cmv .map .sidebar-map, +.cmv .map .cmv-widget, +.cmv .map .scalebar_bottom-left { + transition: left 300ms; +} + +@media (max-width: 320px) { + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 100px; + } +} + +@media (min-width: 321px) and (max-width: 400px) { + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 140px; + } +} + +@media (min-width: 401px) and (max-width: 767px) { + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 160px; + } +} + +@media (max-width: 767px) { + .cmv .sidebar-widget .sidebar-widget-content { + zoom: 1.25; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .cmv .sidebar { + width: 300px; + } + .cmv .map .sidebar-map { + left: 300px; + } + .cmv .map .cmv-widget-mapinfo { + left: 300px; + } + .cmv .map .cmv-widgets-left { + left: 314px; + } + .cmv .map .scalebar_bottom-left { + left: 325px !important; + } + .cmv .map .cmv-widget-geocoder { + left: 360px; + } + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 140px; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .cmv .sidebar { + width: 350px; + } + .cmv .map .sidebar-map { + left: 350px; + } + .cmv .map .cmv-widget-mapinfo { + left: 350px; + } + .cmv .map .cmv-widgets-left { + left: 364px; + } + .cmv .map .cmv-widget-geocoder { + left: 410px; + } + .cmv .map .scalebar_bottom-left { + left: 375px !important; + } + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 190px; + } +} + +@media (min-width: 1200px) and (max-width: 1800px) { + .cmv .sidebar { + width: 400px; + } + .cmv .map .sidebar-map { + left: 400px; + } + .cmv .map .cmv-widget-mapinfo { + left: 400px; + } + .cmv .map .cmv-widgets-left { + left: 414px; + } + .cmv .map .cmv-widget-geocoder { + left: 460px; + } + .cmv .map .scalebar_bottom-left { + left: 425px !important; + } + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 240px; + } +} + +@media (min-width: 1800px) { + .cmv .sidebar { + width: 450px; + } + .cmv .map .sidebar-map { + left: 450px; + } + .cmv .map .cmv-widget-mapinfo { + left: 450px; + } + .cmv .map .cmv-widgets-left { + left: 464px; + } + .cmv .map .cmv-widget-geocoder { + left: 510px; + } + .cmv .map .scalebar_bottom-left { + left: 475px !important; + } + .cmv .sidebar .arcgisSearch .searchGroup .searchInput { + width: 290px; + } +} + +.cmv .sidebar-collapsed .sidebar-map { + left: 40px; +} + +.cmv .sidebar-collapsed .cmv-widget-mapinfo { + left: 40px; +} + +.cmv .sidebar-collapsed .cmv-widgets-left { + left: 54px; +} + +.cmv .sidebar-collapsed .cmv-widget-geocoder { + left: 100px; +} + +.cmv .sidebar-collapsed .scalebar_bottom-left { + left: 65px !important; +} + +/* simple zoom slider: positioning */ +.cmv .map .esriSimpleSliderTL { + left: 15px; + top: 15px; +} + +.cmv .map .esriSimpleSliderTR { + right: 15px; + top: 15px; +} + +/* simple zoom slider: horizontal */ + +.cmv .map .esriSimpleSliderHorizontal { + top: 10px; +} + +.cmv .map .esriSimpleSliderHorizontal.esriSimpleSliderBL { + left: 10px; +} + +.cmv .map .esriSimpleSliderHorizontal.esriSimpleSliderBR { + right: 10px; +} + +.cmv .map .cmv-widget-basemaps { + right: 10px; + top: 10px; +} \ No newline at end of file diff --git a/viewer/js/viewer/sidebar/templates/Sidebar.html b/viewer/js/viewer/sidebar/templates/Sidebar.html new file mode 100644 index 000000000..025e27262 --- /dev/null +++ b/viewer/js/viewer/sidebar/templates/Sidebar.html @@ -0,0 +1,9 @@ +