WebCenter Sites 12c and a Custom Pick Asset Attribute Editor

image
The Sites 12c documentation has some instructions and examples of setting up a custom attribute editor, but they don't get very complicated. I wanted to add some functionality to the built in editor for picking an asset and realized there are a lot of steps so blogging about it might be usefull for others. Building UI enchancements can feel like putting together a puzzle for the first time. This won't show a full working example but I'll point out all the files and key things that had to be done to add a button next to each selected attribute whether or not the attribute is set to single or multi value.
 
If you want a complete example of a working attribute editor another Function1 blog post covers creating one from start to finish.
 
The first few steps are the same as just about any tutorial on building a custom attribute editor. It gets interesting once you go looking for where to actually change the html to add buttons or other changes. The attribute editor itself is a modified version of the element OpenMarket/Gator/AttributeTypes/PICKFROMTREENew from the ElementCatalog in WebCenter sites. Start by creating an attribute editor in the UI and calling it CustomPickAsset, making it for type asset and putting in the following xml.
 
<?XML VERSION="1.0"?>

 <PRESENTATIONOBJECT NAME="CUSTOMPICKASSET">

 <CUSTOMPICKASSET />

 </PRESENTATIONOBJECT>
 
I also edited the presentationobjects.dtd file, in my jsk it was located at apache-tomcat-7.0.65-sites/webapps/sites/WEB-INF/sites. I updated the PRESENTATIONOBJECT element by adding "| CUSTOMPICKASSET" at the end and added a new section after the other editors later in the file. That section is as follows.
 
<!-- CUSTOMPICKASSET: Custom Element-->
<!ELEMENT CUSTOMPICKASSET ANY>
<!ATTLIST CUSTOMPICKASSET MAXVALUES CDATA #IMPLIED>
<!ATTLIST CUSTOMPICKASSET DISPLAYELEMENT CDATA #IMPLIED>
 
Using eclipse I created an xml element named OpenMarket/Gator/AttributeTypes/CUSTOMPICKASSET which references another element that I created called OpenMarket/Gator/AttributeTypes/CUSTOMPICKASSET1. The xml code is below. The referenced element is my modified PICKFROMTREENew element code.
 
<?xml version="1.0" ?>

<!DOCTYPE FTCS SYSTEM "futuretense_cs.dtd">

<FTCS Version="1.1">

<!-- OpenMarket/Gator/AttributeTypes/CUSTOMPICKASSET

-

- INPUT

-

- OUTPUT

-

-->

<CALLELEMENT NAME="OpenMarket/Gator/AttributeTypes/CUSTOMPICKASSET1"/>

</FTCS>
 
I also created an element OpenMarket/Gator/AttributeTypes/CUSTOMPICKASSET1 as a jsp copying over the code from PICKFROMTREENew and modifying it. From here I wanted to add a button next to each item on the list. The built in attribute editor only shows a text element with the selected elements name. Up to this point all the steps are the same as in 11.1.1.8. Anything after here has only been done in 12c. This is where things get interesting.
 
Now we need to make changes to the TypeAheadWidget element loaded in the code below. This would be located in the code we copied over from the PICKFROMTREENew element.
 
<ics:callelement element='OpenMarket/Gator/FlexibleAssets/Common/TypeAheadWidget'> 

<ics:argument name="parentType" value='<%=pickTypeSB.toString()%>'/>

<ics:argument name="subTypesForWidget" value='<%=sbTypeLucene.toString()%>'/>

<ics:argument name="subTypesForSearch" value='<%=ics.GetVar("subTypesForLucene")%>'/>

<ics:argument name="multipleVal" value='<%=ics.GetVar("isPickMultiple")%>'/>

<ics:argument name="widgetValue" value='<%=pickAssetVals.toString()%>'/> 

<ics:argument name="funcToRun" value='<%="typeAheadPickAssetSc_" + ics.GetVar("AttrID")%>'/>

<ics:argument name="widgetNode" value='<%="typeAheadPickAsset_" + ics.GetVar("AttrID")%>'/>

<ics:argument name="typesForSearch" value='<%=ics.GetVar("attrassettype")%>'/>

<ics:argument name="multiOrderedAttr" value='true'/>

<ics:argument name="displaySearchbox" value='<%=ics.GetVar("disInputbox")%>'/> 

<ics:argument name="maxValsSetting" value='<%=ics.GetVar("MAXVALUES")%>'/>

<ics:argument name="displayElement" value='<%=ics.GetVar("DISPLAYELEMENT")%>'/> 

</ics:callelement>
 
I created a new element called OpenMarket/Gator/FlexibleAssets/Common/CustomTypeAheadWidget starting with the code from the TypeAheadWidget and updated the section above to point to my new element. To reference the new javascript files/classes that will be created a little later on, I added a script tag to the typeaheadwidget code like the one below. The 3 new javascript files we create will need to be deployed under the js folder on the sites server. The files will go in js/extensions/dijit. If extensions/dijit is not there you'll have to create the directories.
 
<script>

dojo.require('extensions.dijit.CustomAssetItemMarkup');

dojo.require('extensions.dijit.CustomAssetSource');

     dojo.require('extensions.dijit.CustomPickAssetContainer');

</script>
 
The widget code is almost all js as it creates a dojo widget. Most of the code we need to modify is going to be copied straight from various js files shipped with Sites. There is a line from the file above that looks like var typeAheadWid_<%=ics.GetVar("widgetNode")%> = new fw.ui.dijit.form.<%= "false".equalsIgnoreCase(ics.GetVar("displaySearchbox")) ? "AssetContainer" : "TypeAhead" %>({. Change this line to use a custom class with the code below. The function that needs to be modified is postCreate and it was copied from fw/ui/dijit/form.AssetContainer.js.Uncompressed.js.
 
// wrapped by build app

define(["dijit","dojo","dojox", "fw/ui/dijit/form/AssetContainer","dojo/require!fw/util,fw/ui/dijit/form/DropZone,dijit/Tooltip,fw/ui/dijit/form/AssetSource,fw/ui/dijit/form/DndItemMarkup,fw/ui/dijit/form/A11y"], function(dijit,dojo,dojox,AssetContainer){

dojo.provide('extensions.dijit.CustomPickAssetContainer');


dojo.require("fw.util");

dojo.require('fw.ui.dijit.form.DropZone');

dojo.require('dijit.Tooltip');

dojo.require('extensions.dijit.CustomAssetSource');

dojo.require('fw.ui.dijit.form.DndItemMarkup');

dojo.require('fw.ui.dijit.form.A11y');


(function(){


var printObject = function(obj){

var x = 0, str = "";

for (attr in obj){

if(x > 0)

str = str + ", ";

str =  str + attr; 

x++;

}

return str;

};


dojo.declare('extensions.dijit.CustomPickAssetContainer', [dijit._Widget, dijit._CssStateMixin, fw.ui.dijit.form.A11y, fw.ui.dijit.form.AssetContainer], {


postCreate: function() {

var s, dz;


this.inherited(arguments);

//domNode

//comboNode

if(this.accept_definition === '*'){

this.accept_definition = {}; 

this.accept_definition['*'] = true;

this.accept_definition = dojo.toJson(this.accept_definition); 

} 

if (this.isInstanceOf(fw.ui.dijit.form.TypeAhead)) {

var dndSrcparent = this.dndSrcparent = dojo.create('div', {'class':'TypeAhead AssetContainer'}, this.wrapperNode.parentNode, 'last');

}

else {

var dndSrcparent = this.dndSrcparent = dojo.create('div', {'class':'AssetContainer'}, this.domNode.parentNode, 'last');

dojo.style(this.domNode, "display", "none");

}

var dndSourceNode = this.dndSourceNode  = dojo.create('div', {}, dndSrcparent, 'last');


//s = this._source = new fw.ui.dijit.form._TypeAheadMixin(this.domNode, {

s = this._source = new extensions.dijit.CustomAssetSource(dndSourceNode, {

/* add any custom variables you want to pass down here for your logic such as the url intended for ajax calls that i passed in  */

ajaxUrl: this.ajaxUrl,

multiple: this.multiple,

multiOrdered: this.multiOrdered,

maxVals: this.maxVals,

accept: (this.hasOwnProperty('accept') ? this.accept : ['*']),

//accept_definition: (this.hasOwnProperty('accept_definition') ? this.accept_definition : ['*'])

accept_definition: (dojo.isString(this.accept_definition) ? dojo.fromJson(this.accept_definition) : this.accept_definition),

skipForm : true,

displayElement: this.displayElementName,

assetType: this.assetType, 

assetId: this.assetId,

isDropZone: this.isDropZone,

confidence: this.confidence,

containerWidget: this,

uniqueName: this.uniqueName

});

if(this.cs_environment === 'ucform' && this.isDropZone === true){

this.dropZone = dz = new fw.ui.dijit.form.DropZone({ariaLabelId: this.ariaLabelId}); 

dojo.place(dz.domNode, this.dndSourceNode, 'last');

dz.startup();

var displayInfo = {};

displayInfo[fw.util.getString('UI/UC1/JS/AcceptedAssetTypes')] = this._source.accept['*'] ? 'Any' :  printObject(this._source.accept);

displayInfo[fw.util.getString('UI/UC1/JS/AcceptedSubTypes')] = this._source.accept_definition['*'] ? 'Any' :  printObject(this._source.accept_definition);

displayInfo[fw.util.getString('UI/UC1/JS/AcceptsMultiple')] = this.multiple;

var dropZoneTooltip = new dijit.Tooltip({

showDelay: 2000,

connectId: [dz.domNode], 

label: fw.util.createDynamicTooltip(displayInfo),

_onHover: function(evt) {

if(!this._showTimer){

this._showTimer = this.defer(function(){ 

if(evt.clientWidth != 0) { 

// only show the tooltip when dropzone node is ALIVE

this.open(evt); 

}

}, this.showDelay);

}

}

});

this.hideDropZone(); 

}

if(this.isDropZone === false){

dojo.addClass(this.dndSrcparent, 'TopMargin');

}

}

});


}());

});
 
In the listing above, the line s = this._source = new extensions.dijit.CustomAssetSource(dndSourceNode, was originally s = this._source = new extensions.dijit.AssetSource(dndSourceNode,. We needed to create a new CustomAssetSource class. Just below that line you can pass any variables you need for your logic in.  I only needed the function _assetSourceCreator for mine. In here is where we see a reference to the final class thats needed, where the ItemMarkup is initialized. The line this._dndItemMarkUp = extensions.dijit.CustomAssetItemMarkup({ is where the final custom class is used.
 
define(["dijit","dojo","dojox","fw/ui/dijit/form/AssetSource", "dojo/require!fw/util,fw/ui/dijit/form/DropZone,fw/ui/dijit/form/AssetItemMarkup,fw/ui/dnd/A11ySource"], function(dijit,dojo,dojox, AssetSource){

dojo.provide('extensions.dijit.CustomAssetSource');


dojo.require("fw.util");

dojo.require('fw.ui.dijit.form.DropZone');

dojo.require('extensions.dijit.CustomAssetItemMarkup');

dojo.require('fw.ui.dnd.A11ySource');


(function(){

var _joinToArray = function(arrays){

var a = arrays[0];

for(var i = 1; i < arrays.length; ++i){

a = a.concat(arrays[i]);

}

return a;

};


dojo.declare('extensions.dijit.CustomAssetSource', [fw.ui.dnd.A11ySource, fw.ui.dijit.form.AssetSource], {


_assetSourceCreator: function(item, hint) {

var createdData = {},

deDivId,

teDivId,

deAttrNode,

teAttrNode;

if(hint != "avatar"){ 

//TODO: would it make any more sense to run this through

//fw.ui.dnd.util.getNormalizedData and then do stuff regardless?

if (item.tree && item.tree.typeAttr) {

var store = item.tree.model.store,

labelAttr = store.getLabelAttributes()[0], 

i = item.item;

createdData = fw.ui.dnd.util.treeSourceCreator(item, hint);

createdData.node.innerHTML = labelAttr ?

createdData.data[labelAttr] : createdData.data.id;

} else if (item.name) {

//if node is a data item, just make sure it has a readable toString

//implementation (to be used by insertNodes when crafting innerHTML)

//TODO: something more flexible than hard-wiring to name?

item.toString = function() { return this.name; }

createdData = this.defaultCreator(item, hint);

}


this._dndItemMarkUp = extensions.dijit.CustomAssetItemMarkup({

/*once again use this space to pass along any variables you need down to the itemmarkup class such as my ajaxUrl variable */

ajaxUrl: this.ajaxUrl,

data: createdData,

accept: this.accept,

accept_definition: this.accept_definition,

confidence: this.confidence,

multiple: this.multiple 

});

this._dndItemMarkUp.startup();

this._dndItemMarkUp.set('value', createdData.node.innerHTML);


deDivId = "de_" + this.uniqueName + "_" + this.assetType + this.assetId + createdData.data.id;

teDivId = "te_" + this.uniqueName + "_" + this.assetType + this.assetId + createdData.data.id;

deAttrNode = dojo.query('[id=' + deDivId + ']')[0];

teAttrNode = dojo.query('[id=' + teDivId + ']')[0];


var declaredClass = this.containerWidget.declaredClass,

assetTypePicked = createdData.data.type,

dndAssetType = assetTypePicked ?  dojo.isArray(assetTypePicked) ? assetTypePicked[0] : assetTypePicked : "",

attributeEditor = declaredClass.substring(declaredClass.lastIndexOf('.') + 1),

attributeEditorName = ( attributeEditor == 'TypeAhead') ? 'TYPEAHEAD' : 'PICKASSET',

assetTypePath = "CustomElements/"+ djConfig.pubName + "/" + dndAssetType + "/",

sitePath = "CustomElements/"+ djConfig.pubName + "/",

defaultPath = "OpenMarket/Gator/AttributeTypes/" + attributeEditorName;


if(deAttrNode){

this._dndItemMarkUp.set('value', deAttrNode.innerHTML);

dojo.destroy(deAttrNode);

}else{

this._getMarkupText(createdData, assetTypePath + defaultPath, "Display", this._dndItemMarkUp);

if(this._dndItemMarkUp.get('value') == ''){

this._getMarkupText(createdData, sitePath + defaultPath, "Display", this._dndItemMarkUp);

if(this._dndItemMarkUp.get('value') == ''){

this._getMarkupText(createdData, defaultPath, "Display", this._dndItemMarkUp);

}

}

}


if(teAttrNode){

this._dndItemMarkUp.set('tooltipLabel', teAttrNode.innerHTML);

dojo.destroy(teAttrNode);

}else{

this._getMarkupText(createdData, assetTypePath + defaultPath, "Tooltip", this._dndItemMarkUp);

if(this._dndItemMarkUp.get('value') == ''){

this._getMarkupText(createdData, sitePath + defaultPath, "Tooltip", this._dndItemMarkUp);

if(this._dndItemMarkUp.get('value') == ''){

this._getMarkupText(createdData, defaultPath, "Tooltip", this._dndItemMarkUp);

}

}

}


dojo.connect(this._dndItemMarkUp, "onDeleteClick", this, "_onRemove");

dojo.connect(this._dndItemMarkUp, "onEditClick", this, "_onEdit");

dojo.connect(this._dndItemMarkUp, "onConfChange", this, "onConfidenceChange");

createdData.node.innerHTML = '';

dojo.place(this._dndItemMarkUp.domNode, createdData.node, "first");

return createdData;

}

else

{ 

createdData.node = dojo.create('div',{innerHTML: item.name + '</br>' + item.id + '</br>' + item.type});

createdData.data = item;

createdData.type = [item.type];

return createdData;

} 

}

});

}());

});
 
Finally the listing below shows the CustomAssetItemMarkup class. It's here that we can alter the html for the type ahead list items and add handlers for any buttons we add. Near the top a giant html string is built. I added a div near the bottom with a custom example. Below that I added an additional function that would be called when our link is clicked, _onCustomClick. The CustomAssetItemMarkup listing is below.
 
define(["dijit","dojo","dojox", "fw/ui/dijit/form/AssetItemMarkup","dojo/require!fw/ui/dijit/form/DndItemMarkup"], function(dijit,dojo,dojox, AssetItemMarkup){

dojo.provide('extensions.dijit.CustomAssetItemMarkup');


dojo.require('fw.ui.dijit.form.DndItemMarkup');


(function() {


dojo.declare('extensions.dijit.CustomAssetItemMarkup', [fw.ui.dijit.form.DndItemMarkup, fw.ui.dijit.form.AssetItemMarkup], {

templateString: dojo.cache("fw.ui.dijit.form", "templates/AssetItemMarkup.html", 

"<div class='MarkUpNode'>\r\n\t" +

"<div class=\"ContainerNode\" dojoAttachPoint=\"ContainerNode\">\r\n\t\t" +

"<div class=\"WrapperNode\" dojoAttachPoint=\"WrapperNode\" dojoAttachEvent='onmouseover: _onValueMouseOver, onmouseout: _onValueMouseOut'>\r\n\t\t\t" +

"<div class=\"ValueContainerNode\" dojoAttachPoint=\"ValueContainerNode\">\r\n\t\t\t\t" +

"<div class=\"ValueNode\" dojoAttachPoint=\"ValueNode\"></div>\r\n\t\t\t\t" +

"<div class=\"ConfNode\" dojoAttachPoint=\"ConfNode\"></div>\r\n\t\t\t" +

"</div>\r\n\t\t" +

"</div>\r\n\t\t" +

"<div class=\"EditContainerNode\" dojoAttachPoint=\"EditContainerNode\" dojoAttachEvent='onmouseover: _onEditMouseOver, onmouseout: _onEditMouseOut'>\r\n\t\t\t" +

"<div class='EditNode' dojoAttachPoint='EditNode' dojoAttachEvent='ondijitclick: _onEditClick' tabindex='0' wairole=\"button\" role=\"button\" style='position:relative; left: -50px;'></div>\r\n\t\t" +

"</div>\r\n\t" +

"</div>\r\n\t" +

"<div class='DeleteContainerNode' dojoAttachPoint='DeleteContainerNode'>\r\n\t\t" +

"<div class='DeleteNode' dojoAttachPoint='DeleteNode' dojoAttachEvent='ondijitclick: _onDeleteClick' tabindex='0' wairole=\"button\" role=\"button\" style='position:relative; top:-5px;'></div>\r\n\t" +

"</div>\r\n" +

"<div class='CustomContainerNode' dojoAttachPoint='CustomContainerNode'>\r\n\t\t" +

"<a href='#' class='CustomNode' dojoAttachPoint='CustomNode' dojoAttachEvent='ondijitclick: _onCustomClick' tabindex='0' style='position: relative; top: -2px;'>Custom</a>\r\n\t" +

"</div>\r\n" +

"</div>"),


_onCustomClick: function(eventData){

alert("");

var url = this.ajaxUrl + "&action=selection&assetid=" + this.data.data.id + "&assettype=" + this.data.type[0];

var self = this;


// do our ajax call using the url we passed down

dojo.xhrGet({

// The URL to request

url: url,

handleAs: 'json',

// The method that handles the request's successful result

// Handle the response any way you'd like!

load: function(result) {


//do whatever we want with the result here

// can call back into a js function in the jsp if needed. 

}

});

},


onEditClick: function(dndItemNode) {


},


_onEditClick: function() { 

this.onEditClick(this.data.node);

},


_onEditMouseOver: function() {


dijit.hideTooltip(this.WrapperNode);

if (this.isDragInProgress)

this.dijitTooltip.hide();

},


_onEditMouseOut: function() {

this.dijitTooltip.hide();

}

});


})();


});
 
Just making a small customization involves a lot of steps. There may even be some better ways of doing this and some developers who are more knowledgeable about dojo may see some places to improve upon, but this should serve as a good starting point for similar customizations. 

 

Subscribe to Our Newsletter

Stay In Touch