Posts tagged javascript
Playing with titlebar and navigator II
0In last blogpost I wrote about titlebar and menu in it. Also I promised some navigator hacking, which is more complex (and crazy has many javascript code) and amazing, so I decided to write whole blogpost on it. Here we are go!
Navigator jsp is pretty simple, but it's not plus for us, because whole navigator is a component – r:navigator. That should not be a problem, but this component has no documentation and what is much worse, this component's aim is just to view that simple menu with Guest home, Alfresco home, My home and Company home. Nothing less, nothing more, no editings.So what to do with that crap? Let's do some hacking! You can see my result at following picture, I added one more link:
Navigator.jsp is in /jsp/sidebar/ and at start there is just that component:
<r:navigator id="navigator" activeArea="#{NavigationBean.toolbarLocation}" />
This component renders ahref links inside divs, like this:
<div id="navigator">
<div><a href="foo">bar</a></div>
<div><a href="bar">foo</a></div>
. . .
</div>
So I need to insert my own container with my link (both styled some way). ActionLink doesn't take 'class' attribute, so I need to inject that attribute with simple javascript:
<a:booleanEvaluator value="#{BrowseSessionBean.isArchivaceExist}">
<f:verbatim>
<div id="navigatorArchivaceBox" class="sidebarButton" style="background-image: url(/alfresco/images/parts/navigator_grey_gradient_bg.gif);">
</f:verbatim>
<a:actionLink id="navigatorArchivaceLink" value="Archivace" href="#{BrowseSessionBean.archivaceUrl}"/>
<f:verbatim>
<script type="text/javascript">
var link = document.getElementById("navigatorArchivaceLink");
link.setAttribute("class","sidebarButtonLink");
</script>
</div>
</f:verbatim>
</a:booleanEvaluator>
I personally hate that opening div inside verbatim tag and also that closing div, but what I can do else :-/. So this simple hack added my own link into navigator menu, it's clickable, it's styled, that link works, but that box is not selectable. After deploy this probably look like left picture a few rows higher. All that code is placed inside evaluator, because I need to have destination space accessible, to display this link in menu.
So next thing to do is to deselect all boxes and all links after 'my link' is clicked and also select my box and my link. Sounds simple, but it isn't thanks to 'My Alfresco' link. I'm doing all this stuff only when I'm at the right place (my Archivace space), so I need evaluator:
<a:booleanEvaluator value="#{BrowseSessionBean.isInsideArchivace}">
<f:verbatim>
Inside this evaluator I have simple javascript, which goes through elemnts under navigator container – links and divs – and for each set 'unselected' style:
<script type="text/javascript">
// disabling 'selected' style at all links
var links = document.getElementById("navigator").getElementsByTagName('a');
for(var i=0; i<links.length; i++){
links[i].setAttribute("class","sidebarButtonLink");
}// disabling 'selected' style at all boxes
var links = document.getElementById("navigator").getElementsByTagName('div');
for(var i=0; i<links.length; i++){
links[i].setAttribute("class","sidebarButton");
links[i].setAttribute("style","background-image: url(/alfresco/images/parts/navigator_grey_gradient_bg.gif)");
}
Now it's time to set 'selected' style just for my (selected) link. I know its ids, so I can access them directly through getElementById:
document.getElementById("navigatorArchivaceBox").setAttribute("style","background-image: url(/alfresco/images/parts/navigator_blue_gradient_bg.gif)");
document.getElementById("navigatorArchivaceLink").setAttribute("class","sidebarButtonSelectedLink");
And now all I have to do is 'just' to create evaluator – isInsideArchivace. That is not so hard – just getting current node and asking, if it has wanted aspect ;-).
public boolean getIsInsideArchivace(){
if(this.getCurrentLocation()==null)return false;
return this.navigator.getCurrentNode().hasAspect(cz.shmoula.ArchivaceModel.archSummary);
}
Now it's all doomed to work, like on first and second image :-). BUT – 'My Alfresco' is not an ordinary space, it's some virtual space or something, so when I left my 'Archivace' space by clicking on 'My Alfresco', My Alfresco screen is opened, but selected option is still 'Archivace'. What to do? Look at breadcrumb, because when there is node, it's ordinary space. But when there's nothing, it's probably 'My Alfresco' space (if not, it doesn't matter, because it is not my 'Archivace' space) and it's neccessarynot to display 'Archivace' as selected node. Soall that is inside that getCurrentLocation, which fragments are stolen somewhere from Alfresco sources:
public String getCurrentLocation(){
List<IBreadcrumbHandler> location = this.navigator.getLocation();
String name = null;
if (location.size() != 0){
for (int i=0; i<location.size(); i++){
IBreadcrumbHandler element = location.get(i);
if (element instanceof IRepoBreadcrumbHandler){
NodeRef nodeRef = ((IRepoBreadcrumbHandler)element).getNodeRef();
name = Repository.getNameForNode(this.getNodeService(), nodeRef);
}
}
}
return name;
}
At this time it's all and it works! Ugly and hacky solution, but it works (FF3). Maybe it'll be better to write own component (eg. some tree browsing component), but now I wanted some simple and fast (and really ugly). Oh, I almost forget – this thread helped me a lot!
Playing with titlebar and navigator
0My next quest was to change content of titlebar and navigator box. Titlebar was really easy, but navigator box is 'hell itself', because it's r:navigator component and i didn't want to create another component just for adding one more line.
Titlebar editing is very easy. Final result is on following screenshot:

That topmenu is created by a styled a:modeList component with some listItems. ModeList has actionListener set and after listItem is clicked, its value is sent to that listener (which is a big switch). So my first task was to bridge that listener. Original code is placed in NavigationBean and that method is named toolbarLocationChanged. I wrote my own eventListener in 'someBean' this way:
public void toolbarLocationChanged(ActionEvent event){
UIModeList locationList = (UIModeList)event.getComponent();
String location = locationList.getValue().toString();
if(location.equals("archivace")){
. . .
}else this.navigator.processToolbarLocation(location, true);
In this piece of code is loaded 'location' – value of listItem (ie companyhome, userhome, archivace… see later) and is compared with identificator of my action.If equals, something happens (see next listing ;-)), else original processToolbarLocation routine in NavigationBean is called.
That something piece of code, which happens in that case is creation of breadcrumb and some informations in NavigationBean are changed, follows here:
List<IBreadcrumbHandler> elements = new ArrayList<IBreadcrumbHandler>(1);
Node archivace = new Node(getArchivaceRef());
elements.add(new NavigationBreadcrumbHandler(archivace.getNodeRef(), archivace.getName()));
this.navigator.setLocation(elements);
this.navigator.setCurrentNodeId(archivace.getId());
But that is not all, you have to inform registered beans that the current area has changed:
UIContextService.getInstance(FacesContext.getCurrentInstance()).areaChanged();
And because the current node is not added to the dispatch context automaticaly, it's needed to add it programaticaly (this was very old bug of Alfresco and in some cases still lasts – forum, jira):
this.navigator.setupDispatchContext(this.navigator.getCurrentNode());
Last thing to do is to force a navigation to refresh the browse screen breadcrumb:
FacesContext context = FacesContext.getCurrentInstance();
context.getApplication().getNavigationHandler().handleNavigation(context, null, "browse");
That's all java coding. Now some jsp: you have to edit file titlebar.jsp in /jsp/parts/ and change actionListener in a:modeList and add another a:listItem as seen on next listing (simplified):
<a:modeList . . . actionListener="#{SomeBean.toolbarLocationChanged}">
<a:listItem value="archivace" label="Archivace" rendered="#{SomeBean.isArchivaceExist}"/>
</a:modeList>
Ah, I see there is evaluator – isArchivaceExist. It's just an evaluator, which returns true/false – if space I want to view exists, so that listItem is viewed just in case of that space exists.
As I can see, it'll be better to move navigator hacks to next blogpost.
Mimetype detection in upload component
0I posted short topic about my way to uploading files to Alfresco some time before. Now I turned all that code into my own component and added it to Tag Library and also added mimetype detection and some other amazing stuff. I'll try to describe some facts about my implementation in this blogpost, so be encouraged to keep reading ;-).
I'm not going to describe how to build own component, there are many tutorials on web (for example here or here) and it's much simplier than stuff I'm going to write about (many ughly javascript code). So let's begin.Firstly, thanks to component,jsp code is simplified. I compressed whole code into component (form inputs, labels, also js libraries includes), so my code lookslike this (note that javascript part is optional and is there just to disable OK button on page load; also mimetype selector is optional).
<%@ taglib uri="http://www.shmoula.cz/jsf/component/tags" prefix="shmoula" %>
<script type="text/javascript">
function pageLoaded(){
document.getElementById("dialog:finish-button").disabled = true;
}
window.onload = pageLoaded;
</script>. . .
<h:inputHidden id="fileId" value="#{SlowUpload.fileId}" />
<shmoula:upload label="soubor" />
<r:mimeTypeSelector id="mime-type" value="#{SlowUpload.mimeType}" />. . .
FileId parameter does still the same thing – it connects get data with post data – identificator is created and then AJAX call is made, in which goes this id and filename to servelt, which creates a new FileBean and also get mimetype from filename and send it back. You can see this snippet on following listing.
public void doGet(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException{
PrintWriter out = response.getWriter();
try{
// creating a new bean for fileinfo storing and also setting some info
this.fileUploadBean = new FileUploadBean();
this.uploadId = request.getParameter("uploadid");
String filename = request.getParameter("filename");// fetching mimetypeService from SlowUpload bean; more info follows
SlowUpload slowUpload = (SlowUpload)request.getSession().getAttribute("SlowUpload");
MimetypeService mimetypeService = slowUpload.getMimetypeService();
// guessing and returning mimetype
// if mimetype is not recognized, application/octet-stream is returned
out.print(mimetypeService.guessMimetype(filename));
}catch(Exception e){
out.print("FALSE");
}
}
MimetypeService must be injected into SlowUpload bean, so I need some Setters in my class for that and definition in Faces config:
<managed-bean>
<managed-bean-name>SlowUpload</managed-bean-name>
<managed-bean-class>cz.shmoula.SlowUpload</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
. . .
<managed-property>
<property-name>mimetypeService</property-name>
<value>#{MimetypeService}</value>
</managed-property>
. . .
</managed-bean>
Now onComplete function is called thanks to prototype library AJAX calling. In this function a right option in select box is selected just through a simple loop, as you can see:
function callbackFunction(originalRequest){
var vystup = new String(originalRequest.responseText);
if(vystup=="FALSE"){
rollback(1);
alert("Chyba pri uploadu");
}else{
var select = document.getElementById("dialog:dialog-body:mime-type");
for(i=0;i<select.length;i++){
var selectVal = new String(select.options[i].value);
if(selectVal.match(vystup)!=null){
select.options[i].selected = true;
select.selectedIndex = i;
break;
}
}
}
}
And after all this whole formular is sent to servlet via POST method. All this is described in previous post, so you can check it out there. But there is one change – servlet returns success of operation inside hidden input. So I changed servlet this way:
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println(". . .<input type="hidden" id="iframe_status" value=""+this.fileUploadBean.getFileName()+"" />. . .");
On client side is after (un-)successfully file upload called handle_upload() function, which gets value from included iFrame and decides that operation was or was not successfull and call appropriate actions:
function handle_upload(){
var doc = getIframeDocument();
var filename = doc.getElementById("iframe_status").value;
if(filename != "FALSE"){
var fileNameInput = document.getElementById('dialog:dialog-body:fileName');
if(fileNameInput!=null)
fileNameInput.value = filename;
checkButtonState();
}else{
rollback(1);
alert("Chyba pri uploadu!");
}
}
CheckButtonState function does nothing else, than enable OK button for complete submit all those information (if some other conditions came true) and rollback function sets form target to original target – …/dialog/container.jsp.
That's all folks! Simplified procedure of my component activity. I wish I have not to write these hacks and all those things are accessible by developer in alfresco (they are there, but you can't use them outside specified containers or-how-to-name-that) some simple way. Maybe in future I hope!
Libisemi, tvorba bookmarkletu podruhé
0Minule jsem popsal způsob vytvoření bookmarkletu a částečně řídicí a zobrazující (odpovídající) logiku na straně serveru s tím, že se nejdená o finální řešení a má svoje mouchy. Tyto mouchy jsem vychytal a vyzkoušel opravené řešení v praxi a zjistil, že jsem zapomněl na poměrně podstatnou věc – zabezpečení a neumožnění croos domain AJAX požadavků, alespoň u slušných prohlížečů: stávající skript totiž přidal k cizí stránce můj kus kódu, který se snažil připojit na můj (=cizí) server. Takže jsem si naběhl a končil na výjimce "Access to restricted URI denied" code: "1012" nsresult: "0x805303f4 (NS_ERROR_DOM_BAD_URI)". Když už jsem ale minule slíbil pokračování stávajícího způsobu, dokončím popis a potom se pokusím problém vyřešit nejjednodušším možným způsobem.
Protože je člověk od přírody tvor lenošný a já nedělám výjimku svému druhu, nechtělo se mi hrát si httpRequestem a ručně dolaďovat bezpečnostní chybky, optimalizovat… Vzal jsem si proto na pomoc js framework jQuery, který Ajax zvládá za mě a věřím tomu, že mnohem lépe a efektivněji. Hned jsem tímto přístupem zabil dvě mouchy jednou ranou: první moucha je zmíněna na předchozím řádku (nebo spíše hejno much) a ta druhá – v původní verzi bookmarklet fungoval až po druhém kliknutí, což je samozřejmě neomluvitelné. Tuším, že je to způsobeno tím, že při prvním klepnutí se sice skript připojí k dokumentu, ale volané metody se neprovedou s tím, že jsou momentálně neznámé. Divné, ale je tomu tak, po druhém klepnutí na bookmarklet je už všechno v pořádku. Stačí samozřejmě na konec vkládaného skriptu vložit volání potřebných rutin a při jeho přiložení k dokumentu se okamžitě provedou.
javascript:
void((
function(){
var element=document.createElement('script');
element.setAttribute('src','http://www.libisemi.cz/send.js?i='+new Date().getTime());
document.body.appendChild(element);
})()
)
Stejným způsobem využívám i nové volání pomocí jQuery, které zveřejňuji na následujícím výpise. Do dokumentu pomocí bookmarkletu vkládám tento skript (viz. výše) a zároveň i komprimovanou verzi knihovny jQuery, která se nachází v tomtéž souboru, takže stačí jedno klepnutí na bookmarklet a na lokálním stroji vše funguje jako švýcarské hodinky. Co se týká obsahu bookmarkletu, oproti minulé verzi tam přibyl "parametr" getTime(). Ten je tam proto, aby prohlížeč vzal v potaz změny ve skriptu send.js a nenačítal jej z cache; s getTime() je totiž požadovaná adresa vždy jedinečná, čili načítána znovu.
/*
* jQuery JavaScript Library v1.3.1
* http://jquery.com/
*
* zde je vložená komprimovaná verze jQuery knihovny
*/var u=encodeURIComponent(location.href);
$.ajax({
type: "POST",
url: 'http://www.libisemi.cz/addajax',
data: 'url='+u,
success: function(s){
. . .
window.location = "http://www.libisemi.cz/addform/"+id;
. . .
}
});
V tomto případě ovšem pořád přetrvává výše zmíněná chyba z důvodu cross site requestů. Stačí chybu nacpat do googlu a pár řešení vyjede. Našel jsem ovšem efektivnější možnost posílání url a to tak, jak jsem původně chtěl, takže směle skvěle se vrhnu do něj, je to jednodušší a hlavně pohodlnější, než několik skriptů a využívání třeba JSONu.
edit: Chyba lávky! Ještě jednodušší to je. Nepředával jsem parametr url, takže ono zmíněné řešení mě pokoplo správným směrem – CakePHP dokáže využívat parametry u metody get!!! Takže jsem do controlleru přidal jednoduchou bezparametrickou metodu add(), ve které tyto parametry přečtu:
function add(){
if(isset($_GET['vstup']))
$vstup = $_GET['vstup'];
. . .
A teď můžu tento controller bez problému volat i s adresou jako s parametrem (např. http://www.libisemi.cz/add?vstup=http%3A%2F%2Fshmoulicek.unas.cz/ ) . Aneb potvrzuji moje oblíbené rčení i chování: proč dělat věci jednoduše, když jdou dělat i složitě :-).
Libisemi, tvorba bookmarkletu
0Pro potřeby libisemi, "prvního asociálního sociálního serveru" jsem potřeboval vytvořit bookmarklet. Protože to však nešlo udělat jednoduše, jako odeslání adresy pomocí GETu, protože na straně serveru používám framework CakePHP a v něm user friendly URLs ve formátu http://domain/controller/action/param. V tomto případě bych jako parametr nemohl předat http adresu. Z toho důvodu jsem byl nucen poslat tuto informaci metodou POST. A když už takto, tak rovnou asynchronně a počkat si na výsledek. V tomto postu bych se chtěl podělit o získané zkušenosti vzniklé na základě vyřešení vzniklých problémů.
Začnu nejprve rozhraním na straně serveru. Dovolím si vypustit zbytečnosti a defakto celý zápis zobecnit. Vzhůru tedy do Controlleru. Následující funkce (vlastně spíš metoda, protože je umístěna ve třídě rozšiřující AppController, jen mi tohle vznostné oslovení u PHP nesedí ;-)) přijmě data zaslaná (asynchronně) pomocí metody post a pustí se do zpracování, které předá zobrazovací vrstvě.
function addajax(){
// prijeti dat
$formular = $this->params['form'];
$url = $formular['url'];
// vystupni layout chci xml a nechci zobrazovat debug informace
$this->layout = 'xml';
Configure::write('debug',0);
/*
* Dalsi akce, jako je napr. parsovani ziskaneho dokumentu
*/
// zapsani informaci a ulozeni do session
$output[0]['url'] = $url;
$this->Session->write('toSave',$innerHTML);
$sessId = $this->Session->id();
// zapisu jeste cislo session
$output[0]['sessionId'] = $sessId;
// a cele pole predam vrstve View
$this->set('output', $output);
}
Uvnitř předcházející metody vyžaduju zobrazení v layoutu xml, ten je definován následujícím způsobem, který mi vygeneruje dokument typu text/xml, zapíše do něj záhlaví a vloží vlastní strukturu, která je generovaná uvnitř vrstvy View. Celkový výstup je uveden na výpisu níže.
<?php
header('Content-type: text/xml');
echo $xml->header();
?>
<addInfo>
<?php echo $content_for_layout; ?>
</addInfo>
Další činnost prováděná uvnitř metody v Controlleru je vytvoření pole "output", které je předáno vrstvě View. Do prvního prvku tohoto pole ještě přidám požadované url (index "url"), celou tuto konstrukci uložím do session a následovně do toho stejného pole přidám i číslo této session (index "sessionId"). Samotný výstup v podobě XML je generován ve view, kterému předávám data pomocí posledního řádku ve výpisu kontrolleru. View je zhruba následující:
echo $xml->elem('sessId',null,$output[0]['sessionId']);
echo $xml->elem('url',null,$output[0]['url']);
. . .
Tato konstrukce za pomocí helperu Xml vygeneruje tagy sessId a url a vloží do nich dané hodnoty. Výstupem je potom následující XML, které je odesláno zpět skriptu na straně prohlížeče (klienta), který provedl volání.
<?xml version="1.0" encoding="UTF-8" ?>
<addInfo>
<sessId>4f40061d6ffa5e6a4a8f4b64b79eb4d3</sessId>
<url>http://domain/page.htm</url>
</addInfo>
Bookmarklet jsem definoval konstrukcí tzv. anonymní funkce – void((function(){ . . . }()), který provede kód umístěný vevnitř. Tímto kódem vytvářím nový element "script", který připojím ke stávající stránce.
javascript:
void((
function(){
var element=document.createElement('script');
element.setAttribute('src','http://www.libisemi.cz/send.js');
document.body.appendChild(element);
var http=getHttp();
sendPost(http,encodeURIComponent(location.href));
}()
)
Tento skript sestává z metody, která na základě typu prohlížeče vygeneruje tzv. HttpRequest objekt, který je použit pro připojení k serveru a odeslání dat. V prohlížečích Netscape Navigator, Apple Safari a Firefoxu se tento objekt vytváří pomocí konstruktoru objektu XMLHttpRequest(), který je součástí objektu window, takžestačí otestovat jeho existenci. Microsoft samozřejmě musí mít něco extra, takže tento objekt vytváří pomocí ActiveX komponenty, takže pokud nevyjde první testování, otestuju existenci ActiveXObject komponenty. Pokud ani tohle testování nevyjde, ohlásím chybu a uživatel má smůlu.
function getHttp(){
var http = false;
if(window.XMLHttpRequest){
http = new XMLHttpRequest();
}else if(window.ActiveXObject){
http = new ActiveXObject("Microsoft.XMLHTTP");
}else alert("nejde vytvorit request!");
return http;
}
Vytvořený HttpRequest objekt předám funkci, která pomocí něj metodou POST odešle data na server. Pomocí metody onReadyStateChange je ještě nastavena další anonymní funkce, která testuje přijatou odpověď serveru – stav požadavku (readyState), kdy čeká na požadavek, až je kompletní a současně na stavový kód protokolu HTTP 200 – OK. Pokud tato skutečnost nastane, pokračuje ve zpracování, rozparsuje přijaté xml a přesměruje stávající dokument na novou adresu, ke které přiloží získané informace (v tomto případě číslo session, které vygeneroval server na základě přijatých dat POST metodou). Jedná se o jednoduchý AJAX, spousta dalších informací se válí volně na síti, nebo někde v archívu mého blogu.
function sendPost(http,url){
http.open("POST","http://www.libisemi.cz/addajax");
http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
http.setRequestHeader("Connection","close");
http.onreadystatechange = function(){
if(http.readyState == 4 && http.status == 200){
var response = http.responseXML;
// … parsovani odpovedi a ziskani promenne 'id' – cislo session
window.location = "http://www.libisemi.cz/addform/"+id;
}
}
http.send("url="+url);
}
Tímto způsobem je vytvořen (polo-)funkční bookmarklet, který funguje po druhém kliknutí a pouze na stejné doméně. Nemám ale rád polovičatá řešení, tak už mám rozdělané řešení. Tohle bych si ale dovolil popsat až příště, tenhle post mi připadá už dost dlouhý ;-).