Ext JS - Knowledge Base

Petit projet pour jouer avec Ext JS

Juin 2010

Histoire de satisfaire ma curiosité je me suis mis en tête de faire une petite appli Ext JS pour noter quelques bouts d'infos ;)

Je suis parti de rien, juste de google, Ext JS, de leur doc et leur forum. En quelques heures a pris forme la petite appli que vous pouvez télécharger ici.

L'objectif est simple : vous fournir une appli simple, pas trop complexe, utilisant les contrôles de base d'Ext JS afin de vous aider à découvrir ce framework particulièrement puissant !

L'appli s'appelle KB comme Knowledge Base : il s'agit juste d'un petit stockage XML d'infos basiques... snippets de code, urls intéressantes, détails techniques que vous avez mis des heures à trouver pour un usage unique mais qui pourraient servir à nouveau dès vous l'aurez oublié :D etc... ou tout autre usage que vous y trouverez ;)

Aperçu de l'appli

Code JS

Ext.data.Types.Tags = {
    convert: function(v, data) {
        return data.join(', ');
    },
    sortType: function(v) {
        return v;
    },
    type: 'Tags'
};

Ext.apply(Ext.util.Format, {
    kbtext: function(value) {
        value = value.replace(/&/g, '&');
        value = value.replace(/</g, '&lt;');
        value = value.replace(/>/g, '&gt;');
        //value = value.replace(/ /g, '&nbsp;');
        value = value.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
        value = value.replace(/((https?|ftp):\/\/[^ \n'"]+)/gi, '<a href="$1" target="_blank">$1</a>');
        value = value.replace(/\[\[\[\s*((.|\r|\n|\t)*?)\s*\]\]\]\n?/g, '<pre class="kb">$1</pre>');
        value = value.replace(/\[p\[\s*((.|\r|\n|\t)*?)\s*\]p\]/gi, '<p class="kb">$1</p>');
        value = value.replace(/\[\[\s*((.|\r|\n|\t)*?)\s*\]\]/g, '<code class="kb">$1</code>');
        value = value.replace(/''([^']+)''/g, '<b>$1</b>');
        value = value.replace(/\n/g, '<br />');
        return value;
    },
    kbtitle: function(value) {
        value = '<b>'+value+'</b>';
        return value;
    }
});

Ext.onReady(function() {

    /*
    var editor = new Ext.ux.grid.RowEditor({
        saveText: 'Update'
    });
    */

    var proxy = new Ext.data.HttpProxy({
        api: {
            read : '?action=getitems',
            create : '?action=additem',
            update: '?action=moditem',
            destroy: '?action=delitem'
        }
    });

    var Item = Ext.data.Record.create([
        {name: 'id', type : 'int', readOnly: true},
        {name: 'created', type : 'date', readOnly: true},
        {name: 'modified', type : 'date', readOnly: true},
        'title',
        'text',
        {name: 'tags', type : 'Tags'},
    ]);


    // TAGS
    var tags = new Ext.data.JsonStore({
        url: '?action=gettags',
        storeId: 'tags',
        fields: ['tag']
    });
    
    var writer = new Ext.data.JsonWriter({
        encode: true,
        writeAllFields: true
    });

    // ITEMS
    var kbitems = new Ext.data.JsonStore({
        proxy: proxy,
        storeId: 'kbitems',
        idProperty: 'id',
        root: 'items',
        writer: writer,
        fields: Item,
        sortInfo: {field:'title', direction:'ASC'},
        listeners: {
            beforeload: function() {
                kbitems.setBaseParam('q', Ext.getCmp('q').getValue());
                var selected = Ext.getCmp('liste_tags').getSelectedRecords();
                var t = [];
                for(i=0; i<selected.length; i++) {
                    t.push(selected[i].get('tag'));
                }
                kbitems.setBaseParam('tags', t.join("\t"));
            }
        }
    });
    
    // VIEWPORT
    new Ext.Viewport({
        layout: 'border',
        items: [{
            region: 'west',
            collapsible: false,
            title: 'Knowledge Base',
            width: 250,
            items: [{
                xtype: 'panel',
                border: false,
                header: false,
                layout: 'column',
                items: [{
                    columnWidth: 1,
                    xtype: 'textfield',
                    fieldLabel: '',
                    id: 'q',
                    name: 'q'
                }, {
                    xtype: 'button',
                    text: 'Clear',
                    listeners: {
                        click: function() {
                            Ext.getCmp('q').setValue('');
                            kbitems.reload();
                        }
                    }
                }, {
                    xtype: 'button',
                    text: 'Search',
                    listeners: {
                        click: function() {
                            kbitems.reload();
                        }
                    }
                }]
            },{
                xtype: 'panel',
                border: false,
                header: false,
                layout: 'column',
                items: [{
                    columnWidth: 0.34,
                    xtype: 'button',
                    text: 'Refresh',
                    listeners: {
                        click: function(){
                            tags.reload();
                        }
                    }
                }, {
                    columnWidth: 0.33,
                    xtype: 'button',
                    text: 'All tags',
                    listeners: {
                        click: function(){
                            Ext.getCmp('liste_tags').selectRange(0, tags.getCount()-1);
                            kbitems.reload();
                        }
                    }
                }, {
                    columnWidth: 0.33,
                    xtype: 'button',
                    text: 'No tag',
                    listeners: {
                        click: function(){
                            Ext.getCmp('liste_tags').clearSelections();
                            kbitems.reload();
                        }
                    }
                }]
            },{
                xtype: 'listview',
                store: tags,
                id: 'liste_tags',
                simpleSelect: false,
                multiSelect: true,
                columns: [{
                    header: 'Tags',
                    dataIndex: 'tag'
                }],
                listeners: {
                    selectionchange: function(){
                        kbitems.reload();
                    }
                }
            }]
        }, {
            region: 'center',
            title: 'Datas',
            layout: 'fit',
            items: {
                xtype: 'editorgrid',
                border: false,
                store: kbitems,
                id: 'liste_items',
                loadingText: 'Loading ...',
                //plugins: [editor],
                clicksToEdit: 2,
                disableSelection: false,
                sm: new Ext.grid.RowSelectionModel({
                    multipleSelect: true
                }),
                tbar: [{
                    iconCls: 'icon-item-add',
                    text: 'Add',
                    handler: function(){
                        var e = new Item({
                            title: '',
                            text: '',
                            tags: ''
                        });
                        //editor.stopEditing();
                        Ext.getCmp('liste_items').stopEditing();
                        kbitems.insert(0, e);
                        Ext.getCmp('liste_items').getView().refresh();
                        Ext.getCmp('liste_items').getSelectionModel().selectRow(0);
                        //editor.startEditing(0);
                        Ext.getCmp('liste_items').startEditing(0);
                    }
                },{
                    //ref: '../removeBtn',
                    iconCls: 'icon-item-delete',
                    text: 'Remove',
                    //disabled: true,
                    handler: function(){
                        //editor.stopEditing();
                        Ext.getCmp('liste_items').stopEditing();
                        var s = Ext.getCmp('liste_items').getSelectionModel().getSelections();
                        for(var i = 0, r; r = s[i]; i++){
                            kbitems.remove(r);
                        }
                    }
                }],

                columns: [
                    new Ext.grid.RowNumberer()
                ,{
                    header: 'Created',
                    dataIndex: 'created',
                    width: 70,
                    sortable: true,
                    xtype: 'datecolumn',
                    format: 'd/m/Y',
                    hidden: true,
                    isCellEditable: false,
                    editor: {
                        xtype: 'datefield',
                        readOnly: true
                    }
                    //tpl: '{modified:date("d/m/Y")}'
                },{
                    header: 'Modified',
                    dataIndex: 'modified',
                    width: 70,
                    sortable: true,
                    xtype: 'datecolumn',
                    format: 'd/m/Y',
                    hidden: true,
                    isCellEditable: false,
                    editor: {
                        xtype: 'datefield',
                        readOnly: true
                    }
                    //tpl: '{modified:date("d/m/Y")}'
                },{
                    header: 'Title',
                    dataIndex: 'title',
                    width: 140,
                    sortable: true,
                    renderer: Ext.util.Format.kbtitle,
                    editor: {
                        xtype: 'textfield',
                        allowBlank: false
                    }
                    //tpl: '<b>{title}</b>'
                },{
                    header: 'Text',
                    dataIndex: 'text',
                    width: 600,
                    renderer: Ext.util.Format.kbtext,
                    editor: {
                        xtype: 'textarea',
                        height: 200
                    }
                    //tpl: '{text:kbtext}'
                },{
                    header: 'Tags',
                    dataIndex: 'tags',
                    sortable: true,
                    width: 100,
                    editor: {
                        xtype: 'textfield'
                    }
                }]
            }
        }]
    });

    tags.load({
    /*
        callback: function(){
            Ext.getCmp('liste_tags').selectRange(0, tags.getCount()-1);
        }
    */
    });
});

Code PHP

<?php
error_reporting(0);
$kbxml = "kb.xml";
if ( get_magic_quotes_gpc() != 0 ) {
	if ( !empty($_GET) ) {
		recstripslashes($_GET);
	}
	if ( !empty($_POST) ) {
		recstripslashes($_POST);
	}
	if ( !empty($_REQUEST) ) {
		recstripslashes($_REQUEST);
	}
}
if ( isset($_REQUEST['action']) ) {
	$kb = new kb($kbxml);
	$kb->action($_REQUEST['action']);
	echo '{failure:true}';
	exit;
}
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<link rel="stylesheet" type="text/css" href="extjs/resources/css/ext-all.css" />
		<!--link rel="stylesheet" type="text/css" href="extjs/examples/ux/css/RowEditor.css" /-->
		<title>Knowledge Base</title>
		<style type="text/css">
			.icon-item-add {
				background-image: url(add.png) !important;
			}
			.icon-item-delete {
				background-image: url(del.png) !important;
			}
			pre.kb, code.kb {
				color: #338;
				background: #f8f8ff;
				max-height: 200px;
				overflow:hidden;
				overflow-x: hidden;
				overflow-y: auto; 
			}
			p.kb {
				background: #fafafa;
				max-height: 200px;
				overflow:hidden;
				overflow-x: hidden;
				overflow-y: auto;
			}
		</style>
	</head>
	<body>
		<script type="text/javascript" src="extjs/adapter/ext/ext-base.js"></script>
		<script type="text/javascript" src="extjs/ext-all.js"></script>
		<!--script type="text/javascript" src="extjs/examples/ux/RowEditor.js"></script-->
		<script type="text/javascript" src="kb.js"></script>
	</body>
</html>
<?php

/**
 *
 */
class kb {

	/**
	 *
	 * @var int 
	 */
	public $versions_saved = 4;

	/**
	 *
	 * @var string
	 */
	protected $filename = "";

	/**
	 *
	 * @var array
	 */
	protected $items = array();

	/**
	 *
	 * @var array
	 */
	protected $tags = array();

	/**
	 *
	 * @var string
	 */
	protected $parsedata = "";

	/**
	 *
	 * @var int
	 */
	protected $maxint = 0;

	/**
	 *
	 * @var string
	 */
	protected $parseitem = array();

	/**
	 *
	 * @param string $filename
	 */
	public function kb($filename) {
		$this->filename = $filename;
		if ( !file_exists($filename) ) {
			$this->save();
		}
		$this->load();
	}

	/**
	 * 
	 */
	public function load() {
		$this->items = array();
		$parser = xml_parser_create('UTF-8');
		xml_set_object($parser, $this);
		xml_set_element_handler($parser, 'startXML', 'endXML');
		xml_set_character_data_handler($parser, 'charXML');
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
		xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8");
		xml_parse($parser, file_get_contents($this->filename));
		$this->tags = array_keys($this->tags);
		sort($this->tags);
	}

	/**
	 *
	 * @param <type> $parser
	 * @param <type> $name
	 * @param <type> $attr 
	 */
	protected function startXML($parser, $name, $attr) {
		if ( $name == 'item' ) {
			$this->parseitem = array(
				'created' => '0000-00-00',
				'modified' => '0000-00-00',
				'title' => '',
				'text' => '',
				'id' => '',
				'tags' => array(),
			);
		}
		$this->parsedata = '';
	}

	/**
	 *
	 * @param <type> $parser
	 * @param <type> $name
	 */
	protected function endXML($parser, $name) {
		if ( $name == 'tag' ) {
			$this->parseitem['tags'][] = $this->parsedata;
			$this->tags[$this->parsedata] = 1;
		}
		elseif ( $name == 'item' ) {
			$this->items[$this->parseitem['id']] = $this->parseitem;
			if ( $this->maxint < $this->parseitem['id'] ) {
				$this->maxint = $this->parseitem['id'];
			}
		}
		elseif ( in_array($name, array('modified', 'created', 'title', 'text', 'id')) ) {
			$this->parseitem[$name] = $this->parsedata;
		}
	}

	/**
	 *
	 * @param <type> $parser
	 * @param <type> $data
	 */
	protected function charXML($parser, $data) {
		if ( trim($data) != '' ) {
			$this->parsedata .= $data;
		}
	}

	/**
	 * 
	 */
	public function save() {
		$xml = '<?xml version="1.0" encoding="UTF-8"?><kb>';
		$xml .= "<items>\n";
		usort($this->items, "sortitem");
		foreach ( $this->items as $item ) {
			$xml .= "<item>\n";
			$xml .= '  <id>' . $item['id'] . "</id>\n";
			$xml .= '  <created>' . $item['created'] . "</created>\n";
			$xml .= '  <modified>' . $item['modified'] . "</modified>\n";
			$xml .= '  <title><![CDATA[' . $item['title'] . "]]></title>\n";
			$xml .= '  <text><![CDATA[' . $item['text'] . "]]></text>\n";
			$xml .= "  <tags>\n";
			sort($item['tags']);
			foreach ( $item['tags'] as $tag ) {
				$xml .= '    <tag><![CDATA[' . $tag . "]]></tag>\n";
			}
			$xml .= "  </tags>\n";
			$xml .= "</item>\n";
		}
		$xml .= '</items>';
		$xml .= '</kb>';
		if ( file_exists($this->filename) ) {
			copy($this->filename, $this->filename . '.' . date('Ymd.His') . '.bak');
		}
		$saved = glob($this->filename . '*');
		if ( is_array($saved) ) {
			if ( count($saved) > $this->versions_saved ) {
				for ( $i = 0; $i < count($saved) - $this->versions_saved; $i++ ) {
					@unlink($saved[$i]);
				}
			}
		}
		file_put_contents($this->filename, $xml);
	}

	/**
	 *
	 * @param string $action 
	 */
	public function action($action) {
		switch ( $action ) {
			case 'gettags' : $this->_gettags();
				break;
			case 'getitems' : $this->_getitems();
				break;
			case 'delitem' : $this->_delitem();
				break;
			case 'additem' : $this->_additem();
				break;
			case 'moditem' : $this->_moditem();
				break;
			default: exit;
		}
	}

	/**
	 *
	 */
	protected function _delitem() {
		if ( isset($_REQUEST['items']) ) {
			$id = trim(trim($_REQUEST['items']), '"');
			if ( is_numeric($id) && isset($this->items[$id]) ) {
				unset($this->items[$id]);
				$this->save();
				echo '{success:true}';
				exit;
			}
		}
	}

	/**
	 *
	 */
	protected function _moditem() {
		if ( isset($_REQUEST['items']) ) {
			$json = json_decode($_REQUEST['items'], true);
			if ( is_array($json) && isset($json['id']) && isset($this->items[$json['id']]) ) {
				$item = $this->items[$json['id']];
				$item['modified'] = date('Y-m-d');
				$item['tags'] = array();
				if ( isset($json['title']) ) {
					$item['title'] = $json['title'];
				}
				if ( isset($json['text']) ) {
					$item['text'] = $json['text'];
				}
				if ( isset($json['tags']) && !empty($json['tags']) ) {
					if ( is_array($json['tags']) ) {
						$json['tags'] = implode(',', $json['tags']);
					}
					$item['tags'] = array_map('trim', explode(',', $json['tags']));
				}
				if ( !empty($item['title']) ) {
					$this->items[$item['id']] = $item;
					$this->save();
					echo json_encode(array('success' => true, 'items' => $item));
					exit;
				}
			}
		}
	}

	/**
	 *
	 */
	protected function _additem() {
		$item = array(
			'created' => date('Y-m-d'),
			'modified' => date('Y-m-d'),
			'title' => '',
			'text' => '',
			'id' => $this->maxint + 1,
			'tags' => array(),
		);
		if ( isset($_REQUEST['items']) ) {
			$json = json_decode($_REQUEST['items'], true);
			if ( is_array($json) ) {
				if ( isset($json['title']) ) {
					$item['title'] = $json['title'];
				}
				if ( isset($json['text']) ) {
					$item['text'] = $json['text'];
				}
				if ( isset($json['tags']) && !empty($json['tags']) ) {
					if ( is_array($json['tags']) ) {
						$json['tags'] = implode(',', $json['tags']);
					}
					$item['tags'] = array_map('trim', explode(',', $json['tags']));
				}
			}
			if ( !empty($item['title']) ) {
				$this->items[$item['id']] = $item;
				$this->save();
				echo json_encode(array('success' => true, 'items' => $item));
				exit;
			}
		}
	}

	/**
	 *
	 */
	protected function _gettags() {
		$a = array();
		foreach ( $this->tags as $tag ) {
			$a[] = array('tag' => $tag);
		}
		echo json_encode($a);
		exit;
	}

	/**
	 *
	 */
	protected function _getitems() {
		$q = strtolower(isset($_REQUEST['q']) ? $_REQUEST['q'] : '');
		$tags = strtolower(isset($_REQUEST['tags']) ? "\t" . $_REQUEST['tags'] . "\t" : '');
		$result = array();
		if ( !empty($q) || !empty($tags) ) {
			foreach ( $this->items as $item ) {
				$c1 = strtolower($item['title']);
				$c2 = strtolower($item['text']);
				$ok = false;
				if ( empty($item['tags']) ) {
					$ok = true;
				}
				else {
					foreach ( $item['tags'] as $tag ) {
						if ( strpos($tags, "\t" . strtolower($tag) . "\t") !== false ) {
							if ( empty($q) ) {
								$ok = true;
							}
							elseif ( strpos($c1, $q) !== false ) {
								$ok = true;
							}
							elseif ( strpos($c2, $q) !== false ) {
								$ok = true;
							}
							elseif ( strpos($tag, $q) !== false ) {
								$ok = true;
							}
						}
					}
				}
				if ( $ok ) {
					$result[] = $item;
				}
			}
		}
		echo json_encode(array('items' => $result));
		exit;
	}

}

function sortitem($a, $b) {
	$a = strtolower($a['title']);
	$b = strtolower($b['title']);
	return strcmp($a, $b);
}

function recstripslashes(& $array) {
	if ( is_array($array) ) {
		foreach ( $array as $key => $value ) {
			if ( is_array($value) ) {
				recstripslashes($array[$key]);
			}
			else {
				$array[$key] = stripslashes($value);
			}
		}
	}
	else {
		$array = stripslashes($array);
	}
}
?>

Téléchargement

kb.zip