JAVASCRIPT : Ext JS - Knowledge Base (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

Téléchargement

kb.zip (10.84 Mo)

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);
    }
}
?>