Adding a custom action to a callout in SharePoint 2013

This article has been VERY useful. But I wanted something lightly different: I wanted to add a custom action, but also have the “EDIT” button (but not the “SHARE”), and to use the current item info for my custom action.

Here is the result:

// source: https://www.eliostruyf.com/adding-a-custom-action-to-a-callout-in-sharepoint-2013/
// add a special callout action for our library
SP.SOD.executeFunc("callout.js", "Callout", function () {
  var itemCtx = {};
  itemCtx.Templates = {};
  itemCtx.BaseViewID = 'Callout';
  // Define the list template type
  itemCtx.ListTemplateType = 101;
  itemCtx.Templates.Footer = function (itemCtx) {
    // context, custom action function, show the ECB menu (boolean)
    return CalloutRenderFooterTemplate(itemCtx, AddCustomAction, true);
  };
  SPClientTemplates.TemplateManager.RegisterTemplateOverrides(itemCtx);
});

function AddCustomAction (renderCtx, calloutActionMenu) {
  var itemIndex = renderCtx.CurrentItemIdx
  // Add your custom action
  calloutActionMenu.addAction(new CalloutAction ({
    text: "Custom Action",
    tooltip: 'This is your custom action',
    onClickCallback: function() {
      // all the data related to your item are into `renderCtx.ListData.Row[itemIndex]`
      console.log('Callback from custom action');
    }
  }));
  // re-add EDIT action
  calloutActionMenu.addAction(new CalloutAction ({
    text: "Edit",
    onClickCallback: function(event) {
      // use the default action we have when clicking on the filename into the library
      // or call the EditForm if it's a list item and not a library
      DispEx(this, event,'TRUE','FALSE','FALSE','SharePoint.OpenDocuments.3','1','SharePoint.OpenDocuments','','1https://your.sharepoint.com/_layouts/15/WopiFrame.aspx?sourcedoc='+renderCtx.ListData.Row[itemIndex].FileRef+'&action=default','','1','0','0','0x7fffffffffffffff')
    }
  }));
}

How to hide the left navigation bar in Sharepoint without CSS

We can easily find some workarounds to hide the left navigation bar on Sharepoint using CSS…. But I wanted to remove it on a specific page, without using any CSS.

It’s actually pretty simple. You need to add the below tag into your .aspx page:

<asp:Content ContentPlaceHolderID="PlaceHolderLeftNavBar" runat="Server"></asp:Content>

Tested with Sharepoint 2013 On-Promise.

How to cache the CSS Fonts with Sharepoint

If you use your own CSS file with your own CSS fonts, then we’ll notice that Sharepoint doesn’t send cache headers if you use the below CSS code:

@font-face {
  font-family: 'Roboto';
  src: url('/_catalogs/masterpage/css/fonts/Roboto/Regular/Roboto-Regular.woff2') format('woff2'),
       url('/_catalogs/masterpage/css/fonts/Roboto/Regular/Roboto-Regular.woff') format('woff'),
       url('/_catalogs/masterpage/css/fonts/Roboto/Regular/Roboto-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
}

To have this resource sent back from the Sharepoint server with cache headers, you just need to add ?ctag=0 at the end:

@font-face {
  font-family: 'Roboto';
  src: url('/_catalogs/masterpage/css/fonts/Roboto/Regular/Roboto-Regular.woff2?ctag=0') format('woff2'),
       url('/_catalogs/masterpage/css/fonts/Roboto/Regular/Roboto-Regular.woff?ctag=0') format('woff'),
       url('/_catalogs/masterpage/css/fonts/Roboto/Regular/Roboto-Regular.ttf?ctag=0') format('truetype');
  font-weight: 400;
  font-style: normal;
}

Create an Unpublished Content view for the masterpage galery [Sharepoint]

With Sharepoint 2013 I wanted an easy way to list of the pages in my masterpage galery that haven’t been published yet.

You first need to Create a new view named Unpublished Content. You can sort by Name, and scroll down to the Folders settings and choose Show all items without folders.

We now have all our files in the view.

Next, switch to the Library ribbon and choose Modify in SharePoint Designer (Advanced) from the Modify View dropdown.

Sharepoint Designer will open your page.

In Sharepoint Designer, switch to the Home tab and choose Advanced Mode. The file is reloaded.

Search into your code the tag ListViewXml. On this line you should have &lt;Query;&&gt;. Just after it, we need to add:
&lt;Where&gt;&lt;Or&gt;&lt;Eq&gt;&lt;FieldRef Name="_ModerationStatus" /&gt;&lt;Value Type="ModStat"&gt;Draft&lt;/Value&gt;&lt;/Eq&gt;&lt;IsNotNull&gt;&lt;FieldRef Name="CheckoutUser" /&gt;&lt;/IsNotNull&gt;&lt;/Or&gt;&lt;/Where&gt;

Before:

After:
(click to enlarge)

Save, and your view should only show the unpublished content!

(Article inspired by https://thechriskent.com/2013/02/14/unpublished-view/)

Trigger an event when an element is visible in a scrollable area [JavaScript]

EDIT in 2022

It might also be interesting to look at Intersection Observer that is now available in most browsers, and there is also a polyfill.

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    let isVisible=(entry.intersectionRatio===1);
  });
}, {threshold:1});
observer.observe(document.querySelector('#element');

END EDIT

I have this specific need that is to trigger an event when an element becomes visible/invisible into a scrollable area. Here is the code I came to:

/**
 * Create the handler
 * @param {NodeElement} holder The scrollable area to monitor (where the 'scroll' event will be apply)
 */
var VisibleEventListener = function(holder) {
  var _this=this;
  _this.started = false;
  _this.holder = holder;
  // detect if an element is visible
  _this.isScrolledIntoView=function(el) {
    var bndElem = el.getBoundingClientRect();
    var bndHolder = _this.holder.getBoundingClientRect();
    return bndElem.top <= bndHolder.top ? !(bndHolder.top - bndElem.top > bndElem.height) : !(bndElem.bottom - bndHolder.bottom > bndElem.height);
  }
  // permits to deal with the scroll
  _this.scrollHandler=function(e) {
    for (var i=0, len=_this.events.length; i<len; i++) {
      _this.events[i].check();
    }
  }
  _this.events=[];
}
/**
 * Add the visible/invisible event for an element into a scrollable area
 * @param {NodeElement}   element  The element to test
 * @param {String}   listener 'visible' or 'invisible'
 * @param {Function} callback The callback function to be called when the event is triggered
 */
VisibleEventListener.prototype.add = function(element, listener, callback) {
  var _this=this;
  var ElementToMonitor=function(element, listener, callback) {
    this._super=_this;
    this.element=element;
    this.isVisible=false;
    this.callback=callback;
    this.listener=listener;
  }
  ElementToMonitor.prototype.check = function() {
    var visible=this._super.isScrolledIntoView(this.element);
    if (visible && !this.isVisible) { // becomes visible
      this.isVisible=true;
      if (this.listener==='visible') this.callback.call(this.element)
    } else if (!visible && this.isVisible) { // becomes invisible
      this.isVisible=false;
      if (this.listener==='invisible') this.callback.call(this.element)
    }
  };
  var etm=new ElementToMonitor(element,listener,callback);
  _this.events.push(etm);
  // if we have started to monitor
  if (_this.started===true) etm.check();
}
VisibleEventListener.prototype.start = function() {
  this.holder.addEventListener('scroll', this.scrollHandler);
  this.started = true;
  // trigger the check to verify if the elements are already visible
  this.scrollHandler();
}
VisibleEventListener.prototype.stop = function() {
  this.holder.removeEventListener('scroll', this.scrollHandler);
  this.started = false;
}

And to use it:

// initiate our area with VisibleEventListener
var vel = new VisibleEventListener(document.querySelector('#s4-workspace'));
// add the elements to monitor
vel.add(document.querySelector('.slideshow'), 'invisible', function() {
  console.log(this,' is now invisible')
})
vel.add(document.querySelector('.slideshow'), 'visible', function() {
  console.log(this,' is now visible')
})
// start the monitoring
vel.start();

Check if an element is visible [JavaScript]

We can find plenty of answers about how to check if an element is visible.

In my case the one which works with opacity, and into a scrollable area, is this one (that I have optimized):

function isScrolledIntoView(el, holder) {
  var bndElem = el.getBoundingClientRect();
  var bndHolder = holder.getBoundingClientRect();
  return bndElem.top <= bndHolder.top ? !(bndHolder.top - bndElem.top > bndElem.height) : !(bndElem.bottom - bndHolder.bottom > bndElem.height);
}

Resolve Promise one after another, in sequence

Below is the code that is handy when you want to execute several Promise actions in sequence:

function PromiseChain(arr, fct) {
  var dfd = Promise.resolve();
  var res = arr.map(function(item,idx) {
    dfd = dfd.then(function() {
      return fct(item,idx)
    });
    return dfd
  });
  return Promise.all(res)
}

// for short version:
// function PromiseChain(n,r){var e=Promise.resolve(),i=n.map(function(n,i){return e=e.then(function(){return r(n,i)})});return Promise.all(i)}

And an example about how to use it:

function promFunc(key) {
  return new Promise(function(prom_res) {
    setTimeout(function() { prom_res(key) }, 1000);
  })
}

PromiseChain(["a", "b", "c"], function(val, idx) {
  console.log(idx, val);
  return promFunc(idx+"_"+val);
})
.then(function(res) {
  console.log(res);
})

// result:
// 0 a
// 1 b
// 2 c
// Array [ "0_a", "1_b", "2_c" ]

Deep clone an object in JavaScript

We can find many discussions and solutions about this issue.

In my case the one that worked is this code.

Below you’ll find a compressed version of the extend() function (with a fix):

function extend(){var r,t,n,o,e=arguments[0]||{},f=1,i=arguments.length,u=!1,y=function(r){if(null===r||"object"!=typeof r||r.nodeType||null!==r&&r===r.window)return!1;try{if(r.constructor&&!this.hasOwnProperty.call(r.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0};for("boolean"==typeof e&&(u=e,e=arguments[f]||{},f++),"object"!=typeof e&&"function"!=typeof e&&(e={}),!1;i>f;f+=1)if(null!==(r=arguments[f]))for(t in r)e!==r[t]&&"undefined"==typeof e[t]&&(u&&r[t]&&(y(r[t])||(n=Array.isArray(r[t])))?(n?(n=!1,o=e[t]&&Array.isArray(e[t])?e[t]:[]):o=e[t]&&y(e[t])?e[t]:{},e[t]=extend(u,o,r[t])):void 0!==r[t]&&(e[t]=r[t]));return e}

And here an example:

var object_a = {
    value_1: 'a',
    value_2: true,
    value_3: 354,
    value_4: [
      'x',
      'y',
      'z'
    ],
    value_5: {
      I: 'I',
      II: 'II',
      III: 'III',
      IV: {
        a: 'A',
        b: 'B'
      },
      V: [
        'elm', {
          5: 'five'
        },
        ['my_array']
      ]
    },
    value_6: function() {
      return "pouet"
    },
    value_7: new Date(2017, 0, 1)
  };
var object_b = {};
// clone object_a into object_b
extend(true, object_b, object_a);
object_a.value_2 = false;
object_a.value_1 = "b";
console.log("A => ", object_a);
console.log("B => ", object_b);