Integrare un semplice “filebrowser” per ckeditor realizzato con cakephp (versione italiana)

Tra i vari editor html liberi il mio preferito è il popolarissimo ck editor.  Come spesso accade, il componente per la gestione dei file e delle immagini non è gratuito. E’ decisamente completo e ben fatto – ed esistono pure varie alternative di terze parti, di ottimo livello.

Non sempre serve un file manager completo.  A volte è meglio implementare una semplice soluzione ad hoc, avendo controllo completo sulle funzionalità e tutto ciò che gli utenti possono, o non possono, fare.

Poche opzioni per gli utenti e qualche automatismo.

Vediamo, in questa serie di tre o quattro articoli, come uno sviluppatore che usa da poco cakephp ed è un principante per quanto riguarda javascript, ha implementato le funzioni principali per l’inserimento di file, immagini, e media da fonti esterne.

Nell’ordine:

1) File allegati.
Iniettare html personalizzato in ckeditor con un link per scaricare il file (impedendo all’utente di inserire link errati)

2) Immagini.
Poche opzioni per l’utente – immagine piccola allienata a sinistra o destra, o grande al centro, con link alla versione originale.

3) e forse 4) webservices. media da fonti esterne. Usare le api di youtube per cercare ed inserire video, o quelle di Flickr per inserire singole foto o slideshow. beh, è più semplice di quanto sembri.

–Filebrowser di base

Quel che voglio realizzare è un semplice popup con una lista filtrabile dei file e un comando per inserire unlink al file scaricabile in ckeditor.

Nella vista (elemento) in questione, che chiama il “filebrowser”, ho qualcosa del genre:

contents.ctp:

  1. <?php
  2. $javascript->link('ckeditor/ckeditor', false); // started this with cakephp 1.2, so I’m still using javascript and ajax helper..
  3. ?>
  4.  
  5. [..]
  6.  
  7. <?php
  8. echo $form->input('summary', array('type' => 'textarea'));
  9.  
  10. // [..]
  11. echo $html->link($html->image('icons_big/attachment.png',
  12. array('align' => 'absmiddle'))
  13. . __(' Files ', true),
  14.  
  15. 'javascript:;',
  16.  
  17. array('escape' => false ,
  18.  
  19. 'onclick' => "javascript:window.open('".$html->url(array(
  20.  
  21. 'controller' => 'assets',
  22.  
  23. 'action'=>'filebrowser',
  24.  
  25. $Type.'Summary' // opener instance
  26.  
  27. // $Type is the model name (this element is used by different controllers and actions), so this is the rendered form helper field name / id; i.e. NewsSummary, EventSummary, PageWholeContent
  28.  
  29. )
  30.  
  31. )."','_blank', 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=800,height=680'); return false;"
  32. ),
  33. null
  34. );
  35.  
  36. // [..]
  37.  
  38. ?>
  39.  
  40. <script type="text/javascript">
  41.  
  42. var editor = CKEDITOR.replace( "<?php echo $Type ?>Summary" , {customConfig : "/js/ckeditor/alternate_config.js", height : "150px"});
  43.  
  44. CKEDITOR.add
  45.  
  46. CKEDITOR.config.contentsCss = '<?php echo $html->webroot('/js/ckeditor/mycontents.css') ?>' ;
  47.  
  48. </script>

Essenazilmente, una (o più) istanze di ckeditor ed i relativi link per i popup.

Il modello

Niente di speciale, qui.

Dipende molto dalle spcifiche esigenze e preferenze. Io ho questo modello Asset che sostanzialmente imposta la configurazione per il behaviour che effettua l’upload.  In questo caso uso il Media Plugin di David Persson (http://github.com/davidpersson/media), v 0.60, con alcune modifiche.
Ma andrà bene qualsiasi componente / plugin / behaviour per l’upload dei file (e il ridimensionamento delle immagini).

(Preferisco salvare i dati dei file nel database. Non è strettamente necessario ai fini di questo articolo; trovo però che sia molto più semplice -e utile, e veloce- usare una unica tabella per salvare i dati e riferimenti dei file piuttosto che fare la scansione di un albero nel filesystem)

Il Controller

Ecco una versione molto semplificata del codice che uso effettivamente in un controller apposito.

Giusto come riferimento, qui ci sono le principali dichiarazioni:

assets_controller.php:

  1. <?php
  2.  
  3. class AssetsController extends AppController {
  4.  
  5. var $name = 'Assets';
  6.  
  7. var $helpers = array("Html", "Form", "Javascript", "Ajax", "Time", "Text", "Utility", "Media.Medium", "Number");
  8.  
  9. var $components = array('Auth', 'Cookie', 'Toggle', 'Session', 'RequestHandler','Flickr');
  10.  
  11. var $paginate = array('contain' => array(
  12. 'Site' => array('fields' => array('title', 'slug')),
  13. 'Content'=> array('fields' => array('title','slug','publish')))
  14. , 'order' => 'Asset.id DESC'
  15. );
  16.  
  17. ?>

Di seguito, l’azione che gestisce il popup (per filtrare, paginare, caricare e selezionare i file da inserire nel testo)

Anche qui niente di speciale, faccio solo notare il parametro $opener_instance. E’ usato per identificare l’istanza di ckeditor che legata al popup, così da avere la possibilità di istanze multiple (ad esempio introduzione e corpo nel testo, come nel mio caso).

In AssetsController.php:

  1. <?php
  2.  
  3. /**
  4. * file browser, renders the (filtered) popup list of files for the current site
  5. * @param <string> $opener_instance, name of the field / ckeditor instance
  6. * that will get the injected html for image display.
  7. * You can use multiple ck editor instances on teh same page
  8. */
  9.  
  10. function admin_filebrowser($opener_instance,$type='all') {
  11.  
  12. if (isset($this->params['named']['type']))
  13. $type = $this->params['named']['type'];
  14.  
  15. if($this->admin_index($type, true)) {
  16. $this->set('opener_instance', $opener_instance);
  17. $this->render('admin_filebrowser', 'basic');
  18. }
  19.  
  20. }
  21.  
  22. ?>

L’azione passa semplicemente i parametri necessari alla funzione admin_index e imposta le variabili per la view.

admin_index è il nostro tuttofare. Funge sia da normale azione del controller, sia per preparare l’array di valori filtrati / paginati che vengono restituiti all’azione per il filebrowser popup. (e per il “browser” delle immagini, ma questo è un altro post).

In assets_controller.php:

  1. <?php
  2.  
  3. /**
  4. * function admin_index
  5. *
  6. * displays a filtered (by type, search terms, site) lists of files;
  7. *
  8. * used as an action for assets management, or returns data for
  9. * file/imagebrowser used in ckeditor
  10. *
  11. *
  12. * @param <string> $type - type of media
  13. * @param <boolean> $return false, it's the normal asset controller action,
  14. * true, sets data for the popup ckeditor "fielbrowser" and skips
  15. * admin_index view rendering
  16. * @return <boolean>
  17. */
  18.  
  19. function admin_index($type = null, $return = null) {
  20.  
  21. $this->Asset->recursive = 0;
  22.  
  23. // [..]
  24. // all logic for filtering records
  25.  
  26. $this->Asset->contain($this->defaultContain);
  27.  
  28. $assets = $this->paginate(null,$filter);
  29.  
  30. $assets = Sanitize::clean($assets);
  31.  
  32. $this->set('assets', $assets);
  33.  
  34. //[..]
  35. // if it's called by imagebrowser or filebrowser
  36.  
  37. if($return) return true;
  38. // else render index view..
  39.  
  40. }
  41. ?>

Ho tolto le parti non rilevanti (in particolare il codice per filtrare i file in vari modi e la paginazione). Quel che resta è una semplice. funzione per recuperare certi dati. E’ nella view la parte più significativa (è alqaunto semplice, in effetti).

Non ho neanche approfondito le ck editor javascript api o la developer guide. Per mantenere le cose semplici, mi serviva solo una funzione per inserire  html formattato nell’appropriata “textarea” / oggetto ck editor.

In admin_filebrowser.ctp:

  1. <script type="text/javascript">
  2. <!--
  3. function InsertHTML(passed)
  4. {
  5. //get the correct editor object instance (in my case, i.e. if news controller: NewsSummary or
  6. //NewsWholeContent, if events controller -> EventsSummary.. etc. )
  7.  
  8. var oEditor = opener.CKEDITOR.instances.<?php echo $opener_instance ?>;
  9.  
  10. // Check the active editing mode.
  11.  
  12. if ( oEditor.mode == 'wysiwyg' )
  13. {
  14.  
  15. // Insert the desired HTML.
  16. oEditor.insertHtml( passed ) ;
  17.  
  18. }
  19. else
  20. alert('<?php echo __('You must be on WYSIWYG mode!', true); ?>') ;
  21.  
  22. window.close();
  23. }
  24. -->
  25. </script>

Questo è il punto. Questa semplice funzione javascript semplicemente prende l’instanza corretta di ckeditor e inietta nel contenuto della “textarea” quello che gli passiamo, usando il metodo insertHtml().

Tutto ciò ch resta da fare è generare l’html necessario per riempire propriamente la variabile passed. La semplicità sta nel fatto che non serve alcuna azione ajax, ma, essendo nel popup, possiamo fare quel che vogliamo (filtri, paginazione) e generare in php, esclusivamente lato server, il comando javascript necessario.

Quindi, sempre togliendo tutto il codice non rilevante:

In admin_filebrowser.ctp:

  1. <?php
  2. foreach ($assets as $asset):
  3. ?>
  4. <tr>
  5. <td>
  6. <?php echo $asset['Asset']['id']; ?>, <strong><?php echo $asset['Asset']['name']; ?></strong>
  7. </td>
  8. <td>
  9. <?php
  10. // HERE WE ARE
  11. // generated HTML to be embedded in CKeditor
  12. $insert = $html->link(
  13. $html->image('icons_small/attachment.png',
  14. array('width' => 14, 'height' => 14, 'align' => 'absmiddle',
  15. 'alt' => __('attached file', true),
  16. 'title' => __('attached file', true),'border' => 0
  17. )
  18. ) . ' ' . __( $asset['Asset']['name'], true),
  19. array('controller' => 'assets', 'action' => 'show',$asset['Asset']['id'], 'admin' => false),
  20. array('escape' => false),null,false
  21. );
  22.  
  23. //then the javascript lnk that will inject the generated html in ck editor
  24.  
  25. echo $html->link(
  26. $html->image('icons/attachment.png', array('alt' => 'allega', 'title' => 'allega', 'border' => 0, 'align' => 'absmiddle')) .
  27. $html->image('icons/link.png', array('alt' => 'allega', 'title' => 'allega', 'border' => 0, 'align' => 'absmiddle')) .
  28. __('Embed this file', true),
  29.  
  30. //'javascript:;',
  31. 'javascript:InsertHTML(\''. $insert .'\');',
  32.  
  33. array('escape' => false,
  34.  
  35. //'onclick' => 'javascript:InsertHTML(\''. $insert .'\');'
  36. ),
  37. null, false
  38. );
  39. ?>
  40.  
  41. </td>
  42. </tr>
  43. <?php endforeach; ?>

Tutto qua. E’ sufficiente per iniettare quel che vogliamo nel testo. In questo caso, si tratta del nome/titolo del file con un link che punta alla visualizzazione del file (un’azione che, in caso di immagini, le visualizza a risoluzioen originale, oppure usa la media view di cakephp per far scaricare il file).

Ad esempio:

assets_controller .php

  1. <?php
  2. function show ($id) {
  3. $this->view = 'Media';
  4. $this->Asset->recursive = -1;
  5.  
  6. $asset = $this->Asset->find('first', array('conditions' => array('Asset.id' => $id)));
  7.  
  8. //ClassRegistry::init('Inflector');
  9.  
  10. $name = Inflector::slug(substr($asset['Asset']['name'], 0, -3));
  11.  
  12. $download = false;
  13.  
  14. if($asset['Asset']['medium_type'] != 'img') $download = true;
  15.  
  16. $params = array(
  17. 'id' => $asset['Asset']['basename'],
  18. 'name' => $name,
  19. 'download' => $download,
  20. 'extension' => substr($asset['Asset']['basename'], -3),
  21. 'path' => 'media' . DS . $asset['Asset']['dirname'] . DS,
  22. 'cache' => true
  23. );
  24.  
  25. $this->set($params);
  26.  
  27. $this->render();
  28. }
  29.  
  30. ?>
file browser popup

nell’immagine, uno screenshot del popup “filebrowser”.

(si, avrei bisogno di un designer. Davvero. Ma mi manca il budget. Beh, lo stesso template di questo blog è fatto in 30 minuti con artisteer. Siate gentili)

Se qualcuno è interessato, cercherò di pubblicare l’intero codice rilevante “pulito”.Alla fine forse l’intera applicazione verrà rilasciata con una licanza libera. C?è ancora molto tempo..

Nei prossimi articoli della serie: immagini, webservices (yuotube e flickr).

E, come sempre: non sono un super esperto di cakephp – probabilmente ci sono modi migliori per scrivere il codice o per fare le stesse cose. I suggerimenti sono benvenuti