13 Replies Latest reply on May 26, 2015 2:53 PM by mattdickens

    Widget-based UI features/enhancements without customisations

    mattdickens

      My organisation, like many other Jive customers would like to provide our users with the ability to add custom user interface features/enhancements to their places (e.g. tabbed tables, accordions, table column filters, carousels, image navigation) without sacrificing security and without always having to develop plugins that may impede our upgrade path or introduce recurring re-development costs.

      As you know, HTML widgets in groups and projects do not allow non-sys admin users to save JavaScript (OOTB) and where sys admins have taken time to add it, it is usually embedded within the page's content and cannot be used on another group without again requiring the time of a sys admin to edit it. These HTML widgets are also available for editing by group administrators and therefore run the risk of the JavaScript being stripped out by Jive if they try to re-save it as a non-sys admin (again requiring the time of a sys admin to rectify it).

      The other issue we are faced with is how best to store this additional JavaScript/CSS for these enhancements without customising the theme or creating an addon/plugin. This is because our Jive instance doesn't currently have the statics/Manage files functionality enabled due to the security issues associated with it.

       

      I therefore had the following high-level requirements for a set of UI features/enhancements for overview pages:

       

      • Centrally approved, held and maintained code snippets. Stored once, referenced by many overview pages. Upgrades to the widgets need only be made once to be reflected in all groups using them.
      • Separate the JavaScript from the page content such that the snippets are completely generic and can be used on multiple pages.
      • Cannot be edited by non-admins to avoid unintentional stripping of the JavaScript from the widget.
      • Can be added by non-admins to their overview pages.

       

      One possible approach I am proposing and would like your opinion on is to store the JS/CSS assets as attachments on jive documents that are only editable by system admins or other qualified users with the ability to assess and identify any security issues with any submitted snippets before approving them.

      Template group for each of the snippets would then be created that group admins could copy the widgets from onto their own overview pages and customise the content to suit their needs.

       

      The HTML widget on these groups contains JavaScript that loads the specific snippet code as below:

       

      1. Ensure the parent window's jQuery is loaded before attempting to use it.
      2. Make an api call to the snippet's host jive document to obtain the URLs to the latest version of its attached assets.
      3. Load the assets into the parent window to ensure they are not limited to the iframe in which this code is being executed.
      4. Prevent the HTML widget housing this code from being edited except by a system admin.
      5. Hide the HTML widget in read mode so as not to take up any unnecessary screen estate.


      The pic below shows how the code is being loaded by JavaScript in an HTML widget an iframe but injected into the parent page to run on its sibling widget content. It goes without saying that care must be taken in naming our functions and css classes to ensure they do not interfere with the core Jive product.

      widgets flow.PNG


      The screenshot below shows how the widget is displayed on the page in edit mode. Notice the missing 'Edit this widget' option when a non-system admin edits the overview page.

       

      adminonly.png

      <script>
      
          var $;
          injectWhenJQueryIsLoaded("1001"); //this is the DOC ID of the host document with the attached code.
          determineWidgetEditability();
      
          function injectWhenJQueryIsLoaded(hostDoc) {
              if (parent.$j) {
                  $ = parent.$j;
                  $.ajaxSetup({cache: true});
                  var target = "/api/core/v3/contents?filter=entityDescriptor(102," + hostDoc + ")";
                  //Perform an api call to obtain the urls to the latest versions of the host doc's attachments
                  var data = JSON.parse($.ajax({type: "GET", dataType: "text", url: target, async: false}).responseText.replace(/^throw [^;]*;/, ''));
                  if (data.list[0].attachments){
                          $.each(data.list[0].attachments, function(index, attach) {
                              var attName = attach.name;
                              if (attName.substring(attName.length - 4) == ".css") {
                                 $('<link/>', {rel: 'stylesheet', type: 'text/css', href: attach.url}).appendTo('head');
                              } else if (attName.substring(attName.length - 3) == ".js") {
                                  $.getScript(attach.url);  //This will load the script into the parent window rather than this iFrame.
                              }
                          });
                  }
              } else {
                  setTimeout(injectWhenJQueryIsLoaded(hostDoc), 50);
              }
          }
      
          function determineWidgetEditability() {
              framenum = window.frameElement.id.substring(12);
              if (parent.document.getElementById('jive-widgets-panel')) { // If this panel is available in the dom, this overview page is being edited  
                  $('#jive-widgetframe_' + framenum).show(); //Unhide this HTML widget  
                  if (parent.containerType != "14") { //Not a Space! This is a Group or Project overview page. Administrators of Spaces can save JavaScript.  
                      $('#jive-widgetframe-title_' + framenum).text("THIS HTML WIDGET CONTAINS JAVASCRIPT. IT CAN ONLY BE EDITED BY SYSTEM ADMINSTRATORS.");
                      var restEndPoint = "/api/core/v3/admin/properties?count=1"; //This api call is a means to identify if the user is a sys admin.
                      var data = JSON.parse($.ajax({type: "GET", url: restEndPoint, async: false}).responseText.replace(/^throw [^;]*;/, ''));
                      // if system properties can be accessed, this must be an admin so we can allow the widget to be edited, else we'll hide the edit link.  
                      if (data.list){$('#jive-widgetframe-options_' + framenum + ' .jive-widget-edit').show()}else{$('#jive-widgetframe-options_' + framenum + ' .jive-widget-edit').hide()}
                  }
              } else {
                  $('#jive-widgetframe_' + framenum).hide() //Hide this whole html widget frame when overview page is in read mode ...    
              }
          }
      </script>
      

       

      The disadvantages I have identified with this approach are:

      • The entitlement checks required to read the host document and each of the assets.
      • Limited caching of the asset files.
      • Flicker as the DOM is transformed from the original tab (although this could be reduced with some targeted CSS changes to hide the widgets on load and display them once the manipulation has completed).

       

      Re: the entitlement checks - although I accept this is a small overhead, I don't see it as much different from a user adding an image to a document/formatted text widget or adding a 'View document' widget to an overview page. In fact, with the image and view document widget, both require an entitlement check as well as a 'view' to be recorded for them each time they are read.

      I appreciate however that I know very little about the back-end of Jive and so would very much like the opinions of those of you with deeper knowledge. I'd really appreciate your thoughts on this approach and whether you see any major reason we should not use it as a short term measure until such time that we enable the statics/Manage files functionality or implement a plugin that is able inject the snippets directly onto the page upon load.

       

      In anticipation of us enabling the statics/Manage files functionality in our community and for those of you who have it, I have also created an alternative snippet that will load the assets from an open space's statics folder (note that it has to be a space due to the limitation on file types that can be uploaded to the statics folder of a group). This one is a little faster too as it uses plain JavaScript for the initial load instead of having to wait for jQuery to be loaded. The files are also cached which is good if you don't intend to ever upgrade them but a pain if you do as you'll need to rename all the resources and update the references to them in all groups that are using the widget.

      The asset files could of course be stored outside of Jive if you have an available web server but I prefer to keep these type of file dependencies to the same environment in which they are being displayed.

       

      <script>
          injectAsset('/resources/statics/1010/mytabbedtable.js', 'js');
          injectAsset('/resources/statics/1010/tabbedtable.css', 'css');
          determineWidgetEditability();
      
          function injectAsset(assetpath, assettype) {
              if (assettype == 'js') {
                  var asset = parent.document.createElement('script'); asset.setAttribute('src', assetpath); asset.setAttribute('type', 'text/javascript');
              } else if (assettype == 'css') {
                  var asset = parent.document.createElement('link'); asset.setAttribute('href', assetpath); asset.setAttribute('type', 'text/css'); asset.setAttribute('rel', 'stylesheet');
              }
              if (typeof asset != 'undefined') parent.document.getElementsByTagName('head')[0].appendChild(asset)
          }
      
          function determineWidgetEditability() {
              framenum = window.frameElement.id.substring(12);
              if (parent.document.getElementById('jive-widgets-panel')) { // If this panel is available in the dom, this overview page is being edited 
                  parent.document.getElementById('jive-widgetframe_' + framenum).style.display = "block";
                  if (parent.containerType != "14") { //Not a Space! This is a Group or Project overview page. Administrators of Spaces can save JavaScript. 
                      parent.document.getElementById('jive-widgetframe-title_' + framenum).innerHTML = "THIS HTML WIDGET CONTAINS JAVASCRIPT. IT CAN ONLY BE EDITED BY SYSTEM ADMINSTRATORS.";
                      allowEditIfSysAdmin();
                  }
              } else {
                  parent.document.getElementById('jive-widgetframe_' + framenum).style.display = "none"; //Hide this whole html widget frame when overview page is in read mode ...  
              }
          }
      
          function allowEditIfSysAdmin() {
              if (parent.$j) {
                  var $ = parent.$j;
                  var restEndPoint = "/api/core/v3/admin/properties?count=1"; //This api call is a means to identify if the user is a sys admin.
                  $.ajaxSetup({cache: true});
                  var data = JSON.parse($.ajax({type: "GET", url: restEndPoint, async: false}).responseText.replace(/^throw [^;]*;/, ''));
                  // if system properties can be accessed, this must be an admin so we can allow the widget to be edited, else we'll hide the edit link.
                  if (data.list) {
                      $('#jive-widgetframe-options_' + framenum + ' .jive-widget-edit').show()
                  } else {
                      $('#jive-widgetframe-options_' + framenum + ' .jive-widget-edit').hide()
                  }
              } else {
                  setTimeout(allowEditIfSysAdmin(), 50);
              }
          }
      </script>
      

       

      I have a couple of snippets in beta so far which appear to run fine under 7.0.1 and 8.0.0.0 8c4 and I'm happy to share if anyone is interested.

       

      • Add filters and/or a filter box to a Jive table
      • Transform a Jive table of content into a tabbed table of content

       

      The first is an updated version of the code I shared here-> HTML Widget: How to add column filters and search capability to any jive table on an overview page. The functionality is pretty much the same but the code has been enhanced slightly and made compatible with this loading method.


      table filter ani.gif


      The tabbed table snippet allows a simple 2 column table to be transformed into a tabbed table as shown below.

      Column 1 for the label and Column 2 for the tab's content.

      Note the addition of the text '{tabbedtable}' in the table header that the code looks for to identify which tables to work on.


      tabbedtable1.PNG

       

      and the result once the overview page is viewed:

      tabbedtabe2.gif

       

      Ryan Rutan, Nils Drews, Nils Heuer, Matt Collinge + anyone who has experience in this area - I'd very much appreciate your views and thoughts on this approach.

      The specified item was not found. - FYI

        • Re: Widget-based UI features/enhancements without customisations
          gareth.james

          Hi Matt,

           

          Have you heard back from Jive/others on whether this approach is okay?

          I can see a number of benefits this enabling technology would allow us to deliver, so am keen to know if it is going to be permitted.

           

          Gareth

          • Re: Widget-based UI features/enhancements without customisations
            mcollinge

            Really liking those 2 snippet examples you've implemented.. showing them off with animated GIFs also helps! Very neat!!

             

            For things that follow a certain structure that we let people embed into content, we use Render Macros which are added to the Insert menu of the RTE. The advantage of the render macros is that they'll be baked into the page and are cached. They fit certain use-cases, and I don't think your 2 snippets would benefit from being coded like this... hmm, maybe apart from their initial creation, in which case it'd add some sample HTML into the RTE for the user to edit.

             

             

            For something like the button creator, this lets me present the user with configuration options without worrying they'll type the wrong thing, and to give them hint text to guide them.

             

             

            We have other snippets of code that activate based on what's in the content.. pretty much like you're doing. To do that I'm using 2 pieces of code to load things at runtime.. this function adds CSS;

             

            function e14DynaloadCSS(url) {
               
                if (e14DynaloadedCSSBefore(url)) return; // Don't add it again 
               
                if(document.createStyleSheet) {
                    try { document.createStyleSheet(url); } catch (e) {}
                } else {
                    var css = document.createElement('link');
                    css.rel = 'stylesheet';
                    css.type = 'text/css';
                    css.media = 'all';
                    css.href = url;
                    document.getElementsByTagName('head')[0].appendChild(css);
                }
            }
            

             

            And this one is a snippet of jQuery that loads JS and when it's completed, it'll fire up another piece of code (like activating the thing);

             

            $j.getScript("http://admin.brightcove.com/js/BrightcoveExperiences.js", function(data, textStatus, jqxhr) { brightcove.createExperiences(); });
            
            • Re: Widget-based UI features/enhancements without customisations
              mcollinge

              Hi Matt Dickens - I've put together an image gallery extension which works in a similar way to the other snippets you've written. You simply put {gallery} in the table header, then some code (which runs on page load) spots it and extracts images + captions from all the table cells.. these are then pushed into a gallery which replaces the table. You can see it in action in these 2 videos;

               

              This one shows how I've extended the RTE to have a dialog to help the user (not necessary for it to work, but helps users out)

               

               

              This one shows a larger gallery in action

               

               

              Matt; I'd love the code for the 2 things you've shown here. Would you like to trade for this image gallery?

              • Re: Widget-based UI features/enhancements without customisations
                Hauke.Kiesswetter

                the two examples do really look interesting

                • Add filters and/or a filter box to a Jive table
                • Transform a Jive table of content into a tabbed table of content

                Are you able to share the code ?

                  • Re: Widget-based UI features/enhancements without customisations
                    mattdickens

                    The tabbed Table code. This is the latest released version.

                    I'm part way through modifying it further to use the jive column header row colour as the 'selected' tab colour and a 25% shade of it for the unselected tabs. I just need to find some time to finish it.

                     

                    You'll need to use a similar method to load it on the page as I have included in HTML Widget: Search multiple places including sub-places

                     

                    Hope its of some use to you.

                     

                    <!--PwC Tabbed Tables  version 1.4, 20/04/2015
                      Matt Dickens -  Business Solutions, Global Knowledge Services.
                      Transforms standard jive tables into tabbed tables
                    -->
                    
                    
                    <style type="text/css">
                    
                    
                        ._pwcBS_tabs {
                            height: 30px;
                        }
                        ._pwcBS_tabs > ul {
                            font-size: 1em;
                            list-style: none;
                        }
                        ._pwcBS_tabs > ul > li {
                            margin: 0 2px 0 0!important;
                            padding: 7px 10px!important;
                            display: block;
                            float: left;
                            color: #FFF;
                            -webkit-user-select: none;
                            -moz-user-select: none;
                            user-select: none;
                            -moz-border-radius-topleft: 4px;
                            -moz-border-radius-topright: 4px;
                            -moz-border-radius-bottomright: 0px;
                            -moz-border-radius-bottomleft: 0px;
                            border-top-left-radius: 4px;
                            border-top-right-radius: 4px;
                            border-bottom-right-radius: 0px;
                            border-bottom-left-radius: 0px;
                            background: #EEB480;
                        }
                        ._pwcBS_tabs > ul > li:hover {
                            background: #DC6900;
                            cursor: pointer;
                            color: #fff;
                        }
                        ._pwcBS_tabs > ul > li.tabActiveHeader {
                            background: #FFFFFF;
                            cursor: pointer;
                            color: #fff;
                            background: #DC6900;
                        }
                        ._pwcBS_tabscontent {
                            -moz-border-radius-topleft: 0px;
                            -moz-border-radius-topright: 4px;
                            -moz-border-radius-bottomright: 4px;
                            -moz-border-radius-bottomleft: 4px;
                            border-top-left-radius: 0px;
                            border-top-right-radius: 4px;
                            border-bottom-right-radius: 4px;
                            border-bottom-left-radius: 4px;
                            border-left: 1px;
                            padding: 10px 10px 25px;
                            background: #FFFFFF;
                            border: 1px solid #DC6900;
                            margin: 0;
                            color: #333;
                        }
                    </style>
                    
                    
                    <script language="javascript" type="text/javascript">
                        $j(function() {
                            _pwcBS_TTinitialise();
                        });
                    
                        function _pwcBS_TTinitialise() {
                            tabbedtriggertxt = '{tabbedtable}';
                      $j("table:contains('"+tabbedtriggertxt+"')").each(function(t) {
                                var obj = $j(this);
                      $j(this).attr("id","_pwctab_"+t).wrap( "<div id='pwctt"+t+"'></div>" );
                      var tabid=$j(this).attr("id");
                      //t = t + 1;
                      var tabTitle = '', tabContent = '', tabHeaderBegin = "<div id='tabContainer" + t + "'><div class='_pwcBS_tabs'><ul>", tabHeaderMiddle = "", tabHeaderEnd = "</ul></div>", tabContentBegin = "<div class='_pwcBS_tabscontent'>", tabContentMiddle = "", tabContentEnd = "</div></div>";
                      $j('#'+"_pwctab_"+t + ' > tbody > tr').each(function(i) {
                      $j(this).attr('id', 'tabtablerow');
                      tabTitle = $j.trim($j(this).find('td:eq(0)').text());
                      tabHeaderMiddle = tabHeaderMiddle + "<li id='tabHeader_" + i + "'>" + tabTitle + "</li>";
                      tabContent = $j(this).find('td:eq(1)').html();
                      tabContentMiddle = tabContentMiddle + "<div class='tabpage' id='tabpage_" + i + "'>" + tabContent + "</div>";
                      $j(this).attr('id', '');
                      });
                    
                    
                      $j('#pwctt'+t).slideUp("slow", function(){
                      $j('#_pwctab_'+t).replaceWith(tabHeaderBegin + tabHeaderMiddle + tabHeaderEnd + tabContentBegin + tabContentMiddle + tabContentEnd);
                      $j('._pwcBS_tabs ul li:first').attr('class','tabActiveHeader').parent().data('current','1');
                      $j('.tabpage').slice(1).hide();
                      $j('._pwcBS_tabs ul li').on('click', function(e) {
                      $j('._pwcBS_tabs ul li').removeClass("tabActiveHeader");
                      $j('.tabpage').hide();
                      var ident = $(this).attr("id").split("_")[1];
                      $j(this).attr('class','tabActiveHeader').parent().data('current',ident);
                      $j('#tabpage_'+ident).show();
                      })
                      $j('#pwctt'+t).slideDown("slow");
                      });
                            });
                        }
                    </script>