From 50d10d062e2bada1aa3039a8282a4aa04cf46432 Mon Sep 17 00:00:00 2001 From: xhaggi Date: Fri, 14 Sep 2012 15:54:25 +0200 Subject: [PATCH] webui: added new component ItemSelector to select default language(s) --- src/lang_codes.c | 36 +- src/lang_codes.h | 1 + src/webui/extjs.c | 146 +++-- src/webui/static/app/config.js | 55 +- src/webui/static/multiselect/ddview.js | 551 ++++++++++++++++++ src/webui/static/multiselect/multiselect.js | 528 +++++++++++++++++ .../static/multiselect/resources/bottom2.gif | Bin 0 -> 927 bytes .../static/multiselect/resources/down2.gif | Bin 0 -> 920 bytes .../static/multiselect/resources/left2.gif | Bin 0 -> 920 bytes .../multiselect/resources/multiselect.css | 19 + .../static/multiselect/resources/right2.gif | Bin 0 -> 925 bytes .../static/multiselect/resources/top2.gif | Bin 0 -> 927 bytes .../static/multiselect/resources/up2.gif | Bin 0 -> 920 bytes 13 files changed, 1272 insertions(+), 64 deletions(-) create mode 100644 src/webui/static/multiselect/ddview.js create mode 100644 src/webui/static/multiselect/multiselect.js create mode 100644 src/webui/static/multiselect/resources/bottom2.gif create mode 100644 src/webui/static/multiselect/resources/down2.gif create mode 100644 src/webui/static/multiselect/resources/left2.gif create mode 100644 src/webui/static/multiselect/resources/multiselect.css create mode 100644 src/webui/static/multiselect/resources/right2.gif create mode 100644 src/webui/static/multiselect/resources/top2.gif create mode 100644 src/webui/static/multiselect/resources/up2.gif diff --git a/src/lang_codes.c b/src/lang_codes.c index ceb85614..abde7c2a 100644 --- a/src/lang_codes.c +++ b/src/lang_codes.c @@ -554,9 +554,35 @@ const lang_code_t *lang_code_get3 ( const char *code ) } const char **lang_code_split ( const char *codes ) +{ + int i = 0; + const lang_code_t **lcs = lang_code_split2(codes); + const char **ret; + + if(!lcs) return NULL; + + while(lcs[i]) + i++; + + ret = calloc(1+i, sizeof(char*)); + + i = 0; + while(lcs[i]) { + ret[i] = lcs[i]->code2b; + i++; + } + ret[i] = NULL; + free(lcs); + + return ret; +} + +const lang_code_t **lang_code_split2 ( const char *codes ) { int n; - const char *c, *p, **ret; + const char *c, *p; + const lang_code_t **ret; + const lang_code_t *co; /* Defaults */ if (!codes) codes = config_get_language(); @@ -571,19 +597,21 @@ const char **lang_code_split ( const char *codes ) if (*c == ',') n++; c++; } - ret = calloc(2+n, sizeof(char*)); + ret = calloc(2+n, sizeof(lang_code_t*)); /* Create list */ n = 0; p = c = codes; while (*c) { if (*c == ',') { - ret[n++] = lang_code_get(p); + co = lang_code_get3(p); + if(co) + ret[n++] = co; p = c + 1; } c++; } - if (*p) ret[n++] = lang_code_get(p); + if (*p) ret[n++] = lang_code_get3(p); ret[n] = NULL; return ret; diff --git a/src/lang_codes.h b/src/lang_codes.h index 7bfaf3f9..0a79fc19 100644 --- a/src/lang_codes.h +++ b/src/lang_codes.h @@ -36,5 +36,6 @@ const lang_code_t *lang_code_get3 ( const char *code ); /* Split list of codes as per HTTP Language-Accept spec */ const char **lang_code_split ( const char *codes ); +const lang_code_t **lang_code_split2 ( const char *codes ); #endif /* __TVH_LANG_CODES_H__ */ diff --git a/src/webui/extjs.c b/src/webui/extjs.c index b4432c64..9c419001 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -1,6 +1,6 @@ /* * tvheadend, EXTJS based interface - * Copyright (C) 2008 Andreas Öman + * Copyright (C) 2008 Andreas Öman * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,17 +43,19 @@ #include "epg.h" #include "muxer.h" #include "iptv_input.h" - +#include "epggrab/private.h" #include "config2.h" +#include "lang_codes.h" +/** + * + */ static void extjs_load(htsbuf_queue_t *hq, const char *script) { htsbuf_qprintf(hq, - "\n", script); - + "\n", script); } /** @@ -73,7 +75,6 @@ extjs_exec(htsbuf_queue_t *hq, const char *fmt, ...) htsbuf_qprintf(hq, "\r\n\r\n"); } - /** * PVR info, deliver info about the given PVR entry */ @@ -107,6 +108,8 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) extjs_load(hq, "static/app/extensions.js"); extjs_load(hq, "static/livegrid/livegrid-all.js"); extjs_load(hq, "static/lovcombo/lovcombo-all.js"); + extjs_load(hq, "static/multiselect/multiselect.js"); + extjs_load(hq, "static/multiselect/ddview.js"); /** * Create a namespace for our app @@ -172,7 +175,6 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) return 0; } - /** * */ @@ -205,7 +207,6 @@ page_about(http_connection_t *hc, const char *remain, void *opaque) return 0; } - /** * */ @@ -302,8 +303,6 @@ extjs_channels_delete(htsmsg_t *in) channel_delete(ch); } -#include "epggrab/private.h" - /** * */ @@ -483,7 +482,6 @@ extjs_channels(http_connection_t *hc, const char *remain, void *opaque) return 0; } - /** * EPG Content Groups */ @@ -519,7 +517,6 @@ json_single_record(htsmsg_t *rec, const char *root) } - /** * */ @@ -618,7 +615,6 @@ extjs_epggrab(http_connection_t *hc, const char *remain, void *opaque) return 0; } - /** * */ @@ -711,6 +707,66 @@ skip: } +/** + * + */ +static int +extjs_languages(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + const char *op = http_arg_get(&hc->hc_req_args, "op"); + htsmsg_t *out, *array, *e; + + pthread_mutex_lock(&global_lock); + + if(op != NULL && !strcmp(op, "list")) { + + out = htsmsg_create_map(); + array = htsmsg_create_list(); + + const lang_code_t *c = lang_codes; + while (c->code2b) { + e = htsmsg_create_map(); + htsmsg_add_str(e, "identifier", c->code2b); + htsmsg_add_str(e, "name", c->desc); + htsmsg_add_msg(array, NULL, e); + c++; + } + } + else if(op != NULL && !strcmp(op, "config")) { + + out = htsmsg_create_map(); + array = htsmsg_create_list(); + + const lang_code_t **c = lang_code_split2(NULL); + if(c) { + int i = 0; + while (c[i]) { + e = htsmsg_create_map(); + htsmsg_add_str(e, "identifier", c[i]->code2b); + htsmsg_add_str(e, "name", c[i]->desc); + htsmsg_add_msg(array, NULL, e); + i++; + } + free(c); + } + } + else { + pthread_mutex_unlock(&global_lock); + return HTTP_STATUS_BAD_REQUEST; + } + + pthread_mutex_unlock(&global_lock); + + htsmsg_add_msg(out, "entries", array); + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; + +} + /** * */ @@ -909,6 +965,9 @@ extjs_epgrelated(http_connection_t *hc, const char *remain, void *opaque) return 0; } +/** + * + */ static int extjs_epgobject(http_connection_t *hc, const char *remain, void *opaque) { @@ -1201,7 +1260,6 @@ extjs_dvr(http_connection_t *hc, const char *remain, void *opaque) } - /** * */ @@ -1308,8 +1366,6 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) return 0; } - - /** * */ @@ -1327,7 +1383,6 @@ extjs_service_delete(htsmsg_t *in) } } - /** * */ @@ -1364,7 +1419,6 @@ service_update(htsmsg_t *in) } } - /** * */ @@ -1456,8 +1510,6 @@ extjs_servicedetails(http_connection_t *hc, return 0; } - - /** * */ @@ -1501,8 +1553,6 @@ extjs_mergechannel(http_connection_t *hc, const char *remain, void *opaque) return 0; } - - /** * */ @@ -1550,8 +1600,6 @@ service_update_iptv(htsmsg_t *in) } } - - /** * */ @@ -1669,8 +1717,6 @@ extjs_iptvservices(http_connection_t *hc, const char *remain, void *opaque) return 0; } - - /** * */ @@ -1707,7 +1753,6 @@ extjs_service_update(htsmsg_t *in) } } - /** * */ @@ -1801,32 +1846,25 @@ extjs_config(http_connection_t *hc, const char *remain, void *opaque) void extjs_start(void) { - http_path_add("/about.html", NULL, page_about, ACCESS_WEB_INTERFACE); - http_path_add("/extjs.html", NULL, extjs_root, ACCESS_WEB_INTERFACE); - http_path_add("/tablemgr", NULL, extjs_tablemgr, ACCESS_WEB_INTERFACE); - http_path_add("/channels", NULL, extjs_channels, ACCESS_WEB_INTERFACE); - http_path_add("/epggrab", NULL, extjs_epggrab, ACCESS_WEB_INTERFACE); - http_path_add("/channeltags", NULL, extjs_channeltags, ACCESS_WEB_INTERFACE); - http_path_add("/confignames", NULL, extjs_confignames, ACCESS_WEB_INTERFACE); - http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); - http_path_add("/epgrelated", NULL, extjs_epgrelated, ACCESS_WEB_INTERFACE); - http_path_add("/epgobject", NULL, extjs_epgobject, ACCESS_WEB_INTERFACE); - http_path_add("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE); - http_path_add("/dvrlist", NULL, extjs_dvrlist, ACCESS_WEB_INTERFACE); - http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); - http_path_add("/config", NULL, extjs_config, ACCESS_WEB_INTERFACE); - - http_path_add("/mergechannel", - NULL, extjs_mergechannel, ACCESS_ADMIN); - - http_path_add("/iptv/services", - NULL, extjs_iptvservices, ACCESS_ADMIN); - - http_path_add("/servicedetails", - NULL, extjs_servicedetails, ACCESS_ADMIN); - - http_path_add("/tv/adapter", - NULL, extjs_tvadapter, ACCESS_ADMIN); + http_path_add("/about.html", NULL, page_about, ACCESS_WEB_INTERFACE); + http_path_add("/extjs.html", NULL, extjs_root, ACCESS_WEB_INTERFACE); + http_path_add("/tablemgr", NULL, extjs_tablemgr, ACCESS_WEB_INTERFACE); + http_path_add("/channels", NULL, extjs_channels, ACCESS_WEB_INTERFACE); + http_path_add("/epggrab", NULL, extjs_epggrab, ACCESS_WEB_INTERFACE); + http_path_add("/channeltags", NULL, extjs_channeltags, ACCESS_WEB_INTERFACE); + http_path_add("/confignames", NULL, extjs_confignames, ACCESS_WEB_INTERFACE); + http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); + http_path_add("/epgrelated", NULL, extjs_epgrelated, ACCESS_WEB_INTERFACE); + http_path_add("/epgobject", NULL, extjs_epgobject, ACCESS_WEB_INTERFACE); + http_path_add("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE); + http_path_add("/dvrlist", NULL, extjs_dvrlist, ACCESS_WEB_INTERFACE); + http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); + http_path_add("/config", NULL, extjs_config, ACCESS_WEB_INTERFACE); + http_path_add("/languages", NULL, extjs_languages, ACCESS_WEB_INTERFACE); + http_path_add("/mergechannel", NULL, extjs_mergechannel, ACCESS_ADMIN); + http_path_add("/iptv/services", NULL, extjs_iptvservices, ACCESS_ADMIN); + http_path_add("/servicedetails", NULL, extjs_servicedetails, ACCESS_ADMIN); + http_path_add("/tv/adapter", NULL, extjs_tvadapter, ACCESS_ADMIN); #if ENABLE_LINUXDVB extjs_start_dvb(); diff --git a/src/webui/static/app/config.js b/src/webui/static/app/config.js index 37ed6913..2c9b6a86 100644 --- a/src/webui/static/app/config.js +++ b/src/webui/static/app/config.js @@ -1,3 +1,36 @@ +// Store: config languages +tvheadend.languages = new Ext.data.JsonStore({ + autoLoad:true, + root:'entries', + fields: ['identifier','name'], + id: 'identifier', + url:'languages', + baseParams: { + op: 'list' + } +}); + +// Store: all languages +tvheadend.config_languages = new Ext.data.JsonStore({ + autoLoad:true, + root:'entries', + fields: ['identifier','name'], + id: 'identifier', + url:'languages', + baseParams: { + op: 'config' + } +}); + +tvheadend.languages.setDefaultSort('name', 'ASC'); + +tvheadend.comet.on('config', function(m) { + if(m.reload != null) { + tvheadend.languages.reload(); + tvheadend.config_languages.reload(); + } +}); + tvheadend.miscconf = function() { /* * Basic Config @@ -13,13 +46,23 @@ tvheadend.miscconf = function() { var dvbscanPath = new Ext.form.TextField({ fieldLabel : 'DVB scan files path', name : 'muxconfpath', - allowBlank : true + allowBlank : true, + width: 400 }); - var language = new Ext.form.TextField({ - fieldLabel : 'Default Language(s)', - name : 'language', - allowBlank : true + var language = new Ext.ux.ItemSelector({ + name: 'language', + fromStore: tvheadend.languages, + toStore: tvheadend.config_languages, + fieldLabel: 'Default Language(s)', + dataFields:['identifier', 'name'], + msWidth: 190, + msHeight: 150, + valueField: 'identifier', + displayField: 'name', + imagePath: 'static/multiselect/resources', + toLegend: 'Selected', + fromLegend: 'Available' }); /* **************************************************************** @@ -40,7 +83,7 @@ tvheadend.miscconf = function() { } }); - var confpanel = new Ext.FormPanel({ + var confpanel = new Ext.form.FormPanel({ title : 'General', iconCls : 'wrench', border : false, diff --git a/src/webui/static/multiselect/ddview.js b/src/webui/static/multiselect/ddview.js new file mode 100644 index 00000000..e63a67ad --- /dev/null +++ b/src/webui/static/multiselect/ddview.js @@ -0,0 +1,551 @@ +Array.prototype.contains = function(element) { + return this.indexOf(element) !== -1; +}; + +Ext.namespace("Ext.ux"); + +/** + * @class Ext.ux.DDView + * A DnD enabled version of Ext.View. + * @param {Element/String} container The Element in which to create the View. + * @param {String} tpl The template string used to create the markup for each element of the View + * @param {Object} config The configuration properties. These include all the config options of + * {@link Ext.View} plus some specific to this class.
+ *

+ * Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is + * not being performed, the original {@link Ext.data.Record} is removed from the source DDView.
+ *

+ * The following extra CSS rules are needed to provide insertion point highlighting:

 
+.x-view-drag-insert-above { 
+    border-top:1px dotted #3366cc; 
+} 
+.x-view-drag-insert-below { 
+    border-bottom:1px dotted #3366cc; 
+} 
+
+ * + */ +Ext.ux.DDView = function(config) { + if (!config.itemSelector) { + var tpl = config.tpl; + if (this.classRe.test(tpl)) { + config.tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1'); + } + else { + config.tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2'); + } + config.itemSelector = ".x-combo-list-item"; + } + Ext.ux.DDView.superclass.constructor.call(this, Ext.apply(config, { + border: false + })); +}; + +Ext.extend(Ext.ux.DDView, Ext.DataView, { +/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */ +/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */ +/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */ +/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */ + + sortDir: 'ASC', + + isFormField: true, + + classRe: /class=(['"])(.*)\1/, + + tagRe: /(<\w*)(.*?>)/, + + reset: Ext.emptyFn, + + clearInvalid: Ext.form.Field.prototype.clearInvalid, + + msgTarget: 'qtip', + + afterRender: function() { + Ext.ux.DDView.superclass.afterRender.call(this); + if (this.dragGroup) { + this.setDraggable(this.dragGroup.split(",")); + } + if (this.dropGroup) { + this.setDroppable(this.dropGroup.split(",")); + } + if (this.deletable) { + this.setDeletable(); + } + this.isDirtyFlag = false; + this.addEvents( + "drop" + ); + }, + + validate: function() { + return true; + }, + + destroy: function() { + this.purgeListeners(); + this.getEl().removeAllListeners(); + this.getEl().remove(); + if (this.dragZone) { + if (this.dragZone.destroy) { + this.dragZone.destroy(); + } + } + if (this.dropZone) { + if (this.dropZone.destroy) { + this.dropZone.destroy(); + } + } + }, + +/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */ + getName: function() { + return this.name; + }, + +/** Loads the View from a JSON string representing the Records to put into the Store. */ + setValue: function(v) { + if (!this.store) { + throw "DDView.setValue(). DDView must be constructed with a valid Store"; + } + var data = {}; + data[this.store.reader.meta.root] = v ? [].concat(v) : []; + this.store.proxy = new Ext.data.MemoryProxy(data); + this.store.load(); + }, + +/** @return {String} a parenthesised list of the ids of the Records in the View. */ + getValue: function() { + var result = '('; + this.store.each(function(rec) { + result += rec.id + ','; + }); + return result.substr(0, result.length - 1) + ')'; + }, + + getIds: function() { + var i = 0, result = new Array(this.store.getCount()); + this.store.each(function(rec) { + result[i++] = rec.id; + }); + return result; + }, + + isDirty: function() { + return this.isDirtyFlag; + }, + +/** + * Part of the Ext.dd.DropZone interface. If no target node is found, the + * whole Element becomes the target, and this causes the drop gesture to append. + */ + getTargetFromEvent : function(e) { + var target = e.getTarget(); + while ((target !== null) && (target.parentNode != this.el.dom)) { + target = target.parentNode; + } + if (!target) { + target = this.el.dom.lastChild || this.el.dom; + } + return target; + }, + +/** + * Create the drag data which consists of an object which has the property "ddel" as + * the drag proxy element. + */ + getDragData : function(e) { + var target = this.findItemFromChild(e.getTarget()); + if(target) { + if (!this.isSelected(target)) { + delete this.ignoreNextClick; + this.onItemClick(target, this.indexOf(target), e); + this.ignoreNextClick = true; + } + var dragData = { + sourceView: this, + viewNodes: [], + records: [], + copy: this.copy || (this.allowCopy && e.ctrlKey) + }; + if (this.getSelectionCount() == 1) { + var i = this.getSelectedIndexes()[0]; + var n = this.getNode(i); + dragData.viewNodes.push(dragData.ddel = n); + dragData.records.push(this.store.getAt(i)); + dragData.repairXY = Ext.fly(n).getXY(); + } else { + dragData.ddel = document.createElement('div'); + dragData.ddel.className = 'multi-proxy'; + this.collectSelection(dragData); + } + return dragData; + } + return false; + }, + +// override the default repairXY. + getRepairXY : function(e){ + return this.dragData.repairXY; + }, + +/** Put the selections into the records and viewNodes Arrays. */ + collectSelection: function(data) { + data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY(); + if (this.preserveSelectionOrder === true) { + Ext.each(this.getSelectedIndexes(), function(i) { + var n = this.getNode(i); + var dragNode = n.cloneNode(true); + dragNode.id = Ext.id(); + data.ddel.appendChild(dragNode); + data.records.push(this.store.getAt(i)); + data.viewNodes.push(n); + }, this); + } else { + var i = 0; + this.store.each(function(rec){ + if (this.isSelected(i)) { + var n = this.getNode(i); + var dragNode = n.cloneNode(true); + dragNode.id = Ext.id(); + data.ddel.appendChild(dragNode); + data.records.push(this.store.getAt(i)); + data.viewNodes.push(n); + } + i++; + }, this); + } + }, + +/** Specify to which ddGroup items in this DDView may be dragged. */ + setDraggable: function(ddGroup) { + if (ddGroup instanceof Array) { + Ext.each(ddGroup, this.setDraggable, this); + return; + } + if (this.dragZone) { + this.dragZone.addToGroup(ddGroup); + } else { + this.dragZone = new Ext.dd.DragZone(this.getEl(), { + containerScroll: true, + ddGroup: ddGroup + }); +// Draggability implies selection. DragZone's mousedown selects the element. + if (!this.multiSelect) { this.singleSelect = true; } + +// Wire the DragZone's handlers up to methods in *this* + this.dragZone.getDragData = this.getDragData.createDelegate(this); + this.dragZone.getRepairXY = this.getRepairXY; + this.dragZone.onEndDrag = this.onEndDrag; + } + }, + +/** Specify from which ddGroup this DDView accepts drops. */ + setDroppable: function(ddGroup) { + if (ddGroup instanceof Array) { + Ext.each(ddGroup, this.setDroppable, this); + return; + } + if (this.dropZone) { + this.dropZone.addToGroup(ddGroup); + } else { + this.dropZone = new Ext.dd.DropZone(this.getEl(), { + owningView: this, + containerScroll: true, + ddGroup: ddGroup + }); + +// Wire the DropZone's handlers up to methods in *this* + this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this); + this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this); + this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this); + this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this); + this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this); + } + }, + +/** Decide whether to drop above or below a View node. */ + getDropPoint : function(e, n, dd){ + if (n == this.el.dom) { return "above"; } + var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight; + var c = t + (b - t) / 2; + var y = Ext.lib.Event.getPageY(e); + if(y <= c) { + return "above"; + }else{ + return "below"; + } + }, + + isValidDropPoint: function(pt, n, data) { + if (!data.viewNodes || (data.viewNodes.length != 1)) { + return true; + } + var d = data.viewNodes[0]; + if (d == n) { + return false; + } + if ((pt == "below") && (n.nextSibling == d)) { + return false; + } + if ((pt == "above") && (n.previousSibling == d)) { + return false; + } + return true; + }, + + onNodeEnter : function(n, dd, e, data){ + if (this.highlightColor && (data.sourceView != this)) { + this.el.highlight(this.highlightColor); + } + return false; + }, + + onNodeOver : function(n, dd, e, data){ + var dragElClass = this.dropNotAllowed; + var pt = this.getDropPoint(e, n, dd); + if (this.isValidDropPoint(pt, n, data)) { + if (this.appendOnly || this.sortField) { + return "x-tree-drop-ok-below"; + } + +// set the insert point style on the target node + if (pt) { + var targetElClass; + if (pt == "above"){ + dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above"; + targetElClass = "x-view-drag-insert-above"; + } else { + dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below"; + targetElClass = "x-view-drag-insert-below"; + } + if (this.lastInsertClass != targetElClass){ + Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass); + this.lastInsertClass = targetElClass; + } + } + } + return dragElClass; + }, + + onNodeOut : function(n, dd, e, data){ + this.removeDropIndicators(n); + }, + + onNodeDrop : function(n, dd, e, data){ + if (this.fireEvent("drop", this, n, dd, e, data) === false) { + return false; + } + var pt = this.getDropPoint(e, n, dd); + var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.store.getCount() : n.viewIndex; + if (pt == "below") { + insertAt++; + } + +// Validate if dragging within a DDView + if (data.sourceView == this) { +// If the first element to be inserted below is the target node, remove it + if (pt == "below") { + if (data.viewNodes[0] == n) { + data.viewNodes.shift(); + } + } else { // If the last element to be inserted above is the target node, remove it + if (data.viewNodes[data.viewNodes.length - 1] == n) { + data.viewNodes.pop(); + } + } + +// Nothing to drop... + if (!data.viewNodes.length) { + return false; + } + +// If we are moving DOWN, then because a store.remove() takes place first, +// the insertAt must be decremented. + if (insertAt > this.store.indexOf(data.records[0])) { + insertAt--; + } + } + +// Dragging from a Tree. Use the Tree's recordFromNode function. + if (data.node instanceof Ext.tree.TreeNode) { + var r = data.node.getOwnerTree().recordFromNode(data.node); + if (r) { + data.records = [ r ]; + } + } + + if (!data.records) { + alert("Programming problem. Drag data contained no Records"); + return false; + } + + for (var i = 0; i < data.records.length; i++) { + var r = data.records[i]; + var dup = this.store.getById(r.id); + if (dup && (dd != this.dragZone)) { + if(!this.allowDup && !this.allowTrash){ + Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1); + return true + } + var x=new Ext.data.Record(); + r.id=x.id; + delete x; + } + if (data.copy) { + this.store.insert(insertAt++, r.copy()); + } else { + if (data.sourceView) { + data.sourceView.isDirtyFlag = true; + data.sourceView.store.remove(r); + } + if(!this.allowTrash)this.store.insert(insertAt++, r); + } + if(this.sortField){ + this.store.sort(this.sortField, this.sortDir); + } + this.isDirtyFlag = true; + } + this.dragZone.cachedTarget = null; + return true; + }, + +// Ensure the multi proxy is removed + onEndDrag: function(data, e) { + var d = Ext.get(this.dragData.ddel); + if (d && d.hasClass("multi-proxy")) { + d.remove(); + //delete this.dragData.ddel; + } + }, + + removeDropIndicators : function(n){ + if(n){ + Ext.fly(n).removeClass([ + "x-view-drag-insert-above", + "x-view-drag-insert-left", + "x-view-drag-insert-right", + "x-view-drag-insert-below"]); + this.lastInsertClass = "_noclass"; + } + }, + +/** + * Utility method. Add a delete option to the DDView's context menu. + * @param {String} imageUrl The URL of the "delete" icon image. + */ + setDeletable: function(imageUrl) { + if (!this.singleSelect && !this.multiSelect) { + this.singleSelect = true; + } + var c = this.getContextMenu(); + this.contextMenu.on("itemclick", function(item) { + switch (item.id) { + case "delete": + this.remove(this.getSelectedIndexes()); + break; + } + }, this); + this.contextMenu.add({ + icon: imageUrl || AU.resolveUrl("/images/delete.gif"), + id: "delete", + text: AU.getMessage("deleteItem") + }); + }, + +/** Return the context menu for this DDView. */ + getContextMenu: function() { + if (!this.contextMenu) { +// Create the View's context menu + this.contextMenu = new Ext.menu.Menu({ + id: this.id + "-contextmenu" + }); + this.el.on("contextmenu", this.showContextMenu, this); + } + return this.contextMenu; + }, + + disableContextMenu: function() { + if (this.contextMenu) { + this.el.un("contextmenu", this.showContextMenu, this); + } + }, + + showContextMenu: function(e, item) { + item = this.findItemFromChild(e.getTarget()); + if (item) { + e.stopEvent(); + this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true); + this.contextMenu.showAt(e.getXY()); + } + }, + +/** + * Remove {@link Ext.data.Record}s at the specified indices. + * @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove. + */ + remove: function(selectedIndices) { + selectedIndices = [].concat(selectedIndices); + for (var i = 0; i < selectedIndices.length; i++) { + var rec = this.store.getAt(selectedIndices[i]); + this.store.remove(rec); + } + }, + +/** + * Double click fires the event, but also, if this is draggable, and there is only one other + * related DropZone that is in another DDView, it drops the selected node on that DDView. + */ + onDblClick : function(e){ + var item = this.findItemFromChild(e.getTarget()); + if(item){ + if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) { + return false; + } + if (this.dragGroup) { + var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true); + +// Remove instances of this View's DropZone + while (targets.contains(this.dropZone)) { + targets.remove(this.dropZone); + } + +// If there's only one other DropZone, and it is owned by a DDView, then drop it in + if ((targets.length == 1) && (targets[0].owningView)) { + this.dragZone.cachedTarget = null; + var el = Ext.get(targets[0].getEl()); + var box = el.getBox(true); + targets[0].onNodeDrop(el.dom, { + target: el.dom, + xy: [box.x, box.y + box.height - 1] + }, null, this.getDragData(e)); + } + } + } + }, + + onItemClick : function(item, index, e){ +// The DragZone's mousedown->getDragData already handled selection + if (this.ignoreNextClick) { + delete this.ignoreNextClick; + return; + } + + if(this.fireEvent("beforeclick", this, index, item, e) === false){ + return false; + } + if(this.multiSelect || this.singleSelect){ + if(this.multiSelect && e.shiftKey && this.lastSelection){ + this.select(this.getNodes(this.indexOf(this.lastSelection), index), false); + } else if (this.isSelected(item) && e.ctrlKey) { + this.deselect(item); + }else{ + this.deselect(item); + this.select(item, this.multiSelect && e.ctrlKey); + this.lastSelection = item; + } + e.preventDefault(); + } + return true; + } +}); diff --git a/src/webui/static/multiselect/multiselect.js b/src/webui/static/multiselect/multiselect.js new file mode 100644 index 00000000..9cc18329 --- /dev/null +++ b/src/webui/static/multiselect/multiselect.js @@ -0,0 +1,528 @@ +//version 3.0 + +Ext.ux.Multiselect = Ext.extend(Ext.form.Field, { + store:null, + dataFields:[], + data:[], + width:100, + height:100, + displayField:0, + valueField:1, + allowBlank:true, + minLength:0, + maxLength:Number.MAX_VALUE, + blankText:Ext.form.TextField.prototype.blankText, + minLengthText:'Minimum {0} item(s) required', + maxLengthText:'Maximum {0} item(s) allowed', + copy:false, + allowDup:false, + allowTrash:false, + legend:null, + focusClass:undefined, + delimiter:',', + view:null, + dragGroup:null, + dropGroup:null, + tbar:null, + appendOnly:false, + sortField:null, + sortDir:'ASC', + defaultAutoCreate : {tag: "div"}, + + initComponent: function(){ + Ext.ux.Multiselect.superclass.initComponent.call(this); + this.addEvents({ + 'dblclick' : true, + 'click' : true, + 'change' : true, + 'drop' : true + }); + }, + onRender: function(ct, position){ + var fs, cls, tpl; + Ext.ux.Multiselect.superclass.onRender.call(this, ct, position); + + cls = 'ux-mselect'; + + fs = new Ext.form.FieldSet({ + renderTo:this.el, + title:this.legend, + height:this.height, + width:this.width, + style:"padding:1px;", + tbar:this.tbar + }); + if(!this.legend)fs.el.down('.'+fs.headerCls).remove(); + fs.body.addClass(cls); + + tpl = '
'; + + if(!this.store){ + this.store = new Ext.data.SimpleStore({ + fields: this.dataFields, + data : this.data + }); + } + + this.view = new Ext.ux.DDView({ + multiSelect: true, store: this.store, selectedClass: cls+"-selected", tpl:tpl, + allowDup:this.allowDup, copy: this.copy, allowTrash: this.allowTrash, + dragGroup: this.dragGroup, dropGroup: this.dropGroup, itemSelector:"."+cls+"-item", + isFormField:false, applyTo:fs.body, appendOnly:this.appendOnly, + sortField:this.sortField, sortDir:this.sortDir + }); + + fs.add(this.view); + + this.view.on('click', this.onViewClick, this); + this.view.on('beforeClick', this.onViewBeforeClick, this); + this.view.on('dblclick', this.onViewDblClick, this); + this.view.on('drop', function(ddView, n, dd, e, data){ + return this.fireEvent("drop", ddView, n, dd, e, data); + }, this); + + this.hiddenName = this.name; + var hiddenTag={tag: "input", type: "hidden", value: "", name:this.name}; + if (this.isFormField) { + this.hiddenField = this.el.createChild(hiddenTag); + } else { + this.hiddenField = Ext.get(document.body).createChild(hiddenTag); + } + fs.doLayout(); + }, + + initValue:Ext.emptyFn, + + onViewClick: function(vw, index, node, e) { + var arrayIndex = this.preClickSelections.indexOf(index); + if (arrayIndex != -1) + { + this.preClickSelections.splice(arrayIndex, 1); + this.view.clearSelections(true); + this.view.select(this.preClickSelections); + } + this.fireEvent('change', this, this.getValue(), this.hiddenField.dom.value); + this.hiddenField.dom.value = this.getValue(); + this.fireEvent('click', this, e); + this.validate(); + }, + + onViewBeforeClick: function(vw, index, node, e) { + this.preClickSelections = this.view.getSelectedIndexes(); + if (this.disabled) {return false;} + }, + + onViewDblClick : function(vw, index, node, e) { + return this.fireEvent('dblclick', vw, index, node, e); + }, + + getValue: function(valueField){ + var returnArray = []; + var selectionsArray = this.view.getSelectedIndexes(); + if (selectionsArray.length == 0) {return '';} + for (var i=0; i this.maxLength) { + this.markInvalid(String.format(this.maxLengthText, this.maxLength)); + return false; + } + return true; + } +}); + +Ext.reg("multiselect", Ext.ux.Multiselect); + +Ext.ux.ItemSelector = Ext.extend(Ext.form.Field, { + msWidth:200, + msHeight:300, + hideNavIcons:false, + imagePath:"", + iconUp:"up2.gif", + iconDown:"down2.gif", + iconLeft:"left2.gif", + iconRight:"right2.gif", + iconTop:"top2.gif", + iconBottom:"bottom2.gif", + drawUpIcon:true, + drawDownIcon:true, + drawLeftIcon:true, + drawRightIcon:true, + drawTopIcon:true, + drawBotIcon:true, + fromStore:null, + toStore:null, + fromData:null, + toData:null, + displayField:0, + valueField:1, + switchToFrom:false, + allowDup:false, + focusClass:undefined, + delimiter:',', + readOnly:false, + toLegend:null, + fromLegend:null, + toSortField:null, + fromSortField:null, + toSortDir:'ASC', + fromSortDir:'ASC', + toTBar:null, + fromTBar:null, + bodyStyle:null, + border:false, + defaultAutoCreate:{tag: "div"}, + + initComponent: function(){ + Ext.ux.ItemSelector.superclass.initComponent.call(this); + this.addEvents({ + 'rowdblclick' : true, + 'change' : true + }); + }, + + onRender: function(ct, position){ + Ext.ux.ItemSelector.superclass.onRender.call(this, ct, position); + + this.fromMultiselect = new Ext.ux.Multiselect({ + legend: this.fromLegend, + delimiter: this.delimiter, + allowDup: this.allowDup, + copy: this.allowDup, + allowTrash: this.allowDup, + dragGroup: this.readOnly ? null : "drop2-"+this.el.dom.id, + dropGroup: this.readOnly ? null : "drop1-"+this.el.dom.id, + width: this.msWidth, + height: this.msHeight, + dataFields: this.dataFields, + data: this.fromData, + displayField: this.displayField, + valueField: this.valueField, + store: this.fromStore, + isFormField: false, + tbar: this.fromTBar, + appendOnly: true, + sortField: this.fromSortField, + sortDir: this.fromSortDir + }); + this.fromMultiselect.on('dblclick', this.onRowDblClick, this); + + if (!this.toStore) { + this.toStore = new Ext.data.SimpleStore({ + fields: this.dataFields, + data : this.toData + }); + } + this.toStore.on('add', this.valueChanged, this); + this.toStore.on('remove', this.valueChanged, this); + this.toStore.on('load', this.valueChanged, this); + + this.toMultiselect = new Ext.ux.Multiselect({ + legend: this.toLegend, + delimiter: this.delimiter, + allowDup: this.allowDup, + dragGroup: this.readOnly ? null : "drop1-"+this.el.dom.id, + //dropGroup: this.readOnly ? null : "drop2-"+this.el.dom.id+(this.toSortField ? "" : ",drop1-"+this.el.dom.id), + dropGroup: this.readOnly ? null : "drop2-"+this.el.dom.id+",drop1-"+this.el.dom.id, + width: this.msWidth, + height: this.msHeight, + displayField: this.displayField, + valueField: this.valueField, + store: this.toStore, + isFormField: false, + tbar: this.toTBar, + sortField: this.toSortField, + sortDir: this.toSortDir + }); + this.toMultiselect.on('dblclick', this.onRowDblClick, this); + + var p = new Ext.Panel({ + bodyStyle:this.bodyStyle, + border:this.border, + layout:"table", + layoutConfig:{columns:3} + }); + p.add(this.switchToFrom ? this.toMultiselect : this.fromMultiselect); + var icons = new Ext.Panel({header:false}); + p.add(icons); + p.add(this.switchToFrom ? this.fromMultiselect : this.toMultiselect); + p.render(this.el); + icons.el.down('.'+icons.bwrapCls).remove(); + + if (this.imagePath!="" && this.imagePath.charAt(this.imagePath.length-1)!="/") + this.imagePath+="/"; + this.iconUp = this.imagePath + (this.iconUp || 'up2.gif'); + this.iconDown = this.imagePath + (this.iconDown || 'down2.gif'); + this.iconLeft = this.imagePath + (this.iconLeft || 'left2.gif'); + this.iconRight = this.imagePath + (this.iconRight || 'right2.gif'); + this.iconTop = this.imagePath + (this.iconTop || 'top2.gif'); + this.iconBottom = this.imagePath + (this.iconBottom || 'bottom2.gif'); + var el=icons.getEl(); + if (!this.toSortField) { + this.toTopIcon = el.createChild({tag:'img', src:this.iconTop, style:{cursor:'pointer', margin:'2px'}}); + el.createChild({tag: 'br'}); + this.upIcon = el.createChild({tag:'img', src:this.iconUp, style:{cursor:'pointer', margin:'2px'}}); + el.createChild({tag: 'br'}); + } + this.addIcon = el.createChild({tag:'img', src:this.switchToFrom?this.iconLeft:this.iconRight, style:{cursor:'pointer', margin:'2px'}}); + el.createChild({tag: 'br'}); + this.removeIcon = el.createChild({tag:'img', src:this.switchToFrom?this.iconRight:this.iconLeft, style:{cursor:'pointer', margin:'2px'}}); + el.createChild({tag: 'br'}); + if (!this.toSortField) { + this.downIcon = el.createChild({tag:'img', src:this.iconDown, style:{cursor:'pointer', margin:'2px'}}); + el.createChild({tag: 'br'}); + this.toBottomIcon = el.createChild({tag:'img', src:this.iconBottom, style:{cursor:'pointer', margin:'2px'}}); + } + if (!this.readOnly) { + if (!this.toSortField) { + this.toTopIcon.on('click', this.toTop, this); + this.upIcon.on('click', this.up, this); + this.downIcon.on('click', this.down, this); + this.toBottomIcon.on('click', this.toBottom, this); + } + this.addIcon.on('click', this.fromTo, this); + this.removeIcon.on('click', this.toFrom, this); + } + if (!this.drawUpIcon || this.hideNavIcons) { this.upIcon.dom.style.display='none'; } + if (!this.drawDownIcon || this.hideNavIcons) { this.downIcon.dom.style.display='none'; } + if (!this.drawLeftIcon || this.hideNavIcons) { this.addIcon.dom.style.display='none'; } + if (!this.drawRightIcon || this.hideNavIcons) { this.removeIcon.dom.style.display='none'; } + if (!this.drawTopIcon || this.hideNavIcons) { this.toTopIcon.dom.style.display='none'; } + if (!this.drawBotIcon || this.hideNavIcons) { this.toBottomIcon.dom.style.display='none'; } + + var tb = p.body.first(); + this.el.setWidth(p.body.first().getWidth()); + p.body.removeClass(); + + this.hiddenName = this.name; + var hiddenTag={tag: "input", type: "hidden", value: "", name:this.name}; + this.hiddenField = this.el.createChild(hiddenTag); + this.valueChanged(this.toStore); + }, + + initValue:Ext.emptyFn, + + toTop : function() { + var selectionsArray = this.toMultiselect.view.getSelectedIndexes(); + var records = []; + if (selectionsArray.length > 0) { + selectionsArray.sort(); + for (var i=0; i-1; i--) { + record = records[i]; + this.toMultiselect.view.store.remove(record); + this.toMultiselect.view.store.insert(0, record); + selectionsArray.push(((records.length - 1) - i)); + } + } + this.toMultiselect.view.refresh(); + this.toMultiselect.view.select(selectionsArray); + }, + + toBottom : function() { + var selectionsArray = this.toMultiselect.view.getSelectedIndexes(); + var records = []; + if (selectionsArray.length > 0) { + selectionsArray.sort(); + for (var i=0; i 0) { + for (var i=0; i= 0) { + this.toMultiselect.view.store.remove(record); + this.toMultiselect.view.store.insert(selectionsArray[i] - 1, record); + newSelectionsArray.push(selectionsArray[i] - 1); + } + } + this.toMultiselect.view.refresh(); + this.toMultiselect.view.select(newSelectionsArray); + } + }, + + down : function() { + var record = null; + var selectionsArray = this.toMultiselect.view.getSelectedIndexes(); + selectionsArray.sort(); + selectionsArray.reverse(); + var newSelectionsArray = []; + if (selectionsArray.length > 0) { + for (var i=0; i 0) { + for (var i=0; i 0) { + for (var i=0; iEP98sb?%Iv}x9&W=d;iIUM=u^ffBoXs``2$j zefaeC^Vc8WzW)L%A0pdm1C1s+0`OCH>RGR%E_iQqw|2O+bj+? zxf?q#tDc|B%*(-RFl7bfVm3Chh$sby%gg-P`DR#|I)ttYW*53~N>n*~UDW=rzfxsy QA{d!?SIPcRP++hI0IK#;X8-^I literal 0 HcmV?d00001 diff --git a/src/webui/static/multiselect/resources/down2.gif b/src/webui/static/multiselect/resources/down2.gif new file mode 100644 index 0000000000000000000000000000000000000000..15e923445b14e0a5341b96ba9f294e129e105875 GIT binary patch literal 920 zcmZ?wbhEHb6krfw_|5+_f9`Z{2x#_x_Uyk6t`}{`$qM_pjf6 z`ta%N=dVA$eg6ekK1z&+!0-tH#h)yU3=I4XIv_8B@&p4%DT6$RgvW*i$3{LOt(YAJ z3C-;S%5E|z7A!i-z^D@?vtz+x_Z~T0z7U5^D#s?Nva3aOZcIHrm6J_rM&|)lw^?Z56VAa*t;VYBwMulpJuVG@5lCl2a J=)k~W4FHxPQrrLl literal 0 HcmV?d00001 diff --git a/src/webui/static/multiselect/resources/left2.gif b/src/webui/static/multiselect/resources/left2.gif new file mode 100644 index 0000000000000000000000000000000000000000..e8bbfb0537c0d4382fa77e81c242022128395de9 GIT binary patch literal 920 zcmZ?wbhEHb6krfwXlDQcOG`@zo00>EP98sb?%Iv}x9&W=d;iIUM=u^ffBoXs``2$j zefaeC^Vc8WzW)L%A0h4+}H@ zi#0cco;NnKHA!V?Zh3Kl$z$R)pHn3d8kPqzJIuA3&~SBi#Ky3*r>?%bwl0#D!^ETP JL30CxH2`!SR>=SW literal 0 HcmV?d00001 diff --git a/src/webui/static/multiselect/resources/multiselect.css b/src/webui/static/multiselect/resources/multiselect.css new file mode 100644 index 00000000..b93f2b21 --- /dev/null +++ b/src/webui/static/multiselect/resources/multiselect.css @@ -0,0 +1,19 @@ +.ux-mselect{ + overflow:auto; + background:white; + position:relative; /* for calculating scroll offsets */ + zoom:1; + overflow:auto; +} +.ux-mselect-item{ + font:normal 12px tahoma, arial, helvetica, sans-serif; + padding:2px; + border:1px solid #fff; + white-space: nowrap; + cursor:pointer; +} +.ux-mselect-selected{ + border:1px dotted #a3bae9 !important; + background:#DFE8F6; + cursor:pointer; +} diff --git a/src/webui/static/multiselect/resources/right2.gif b/src/webui/static/multiselect/resources/right2.gif new file mode 100644 index 0000000000000000000000000000000000000000..9dba8d78491bf40fcd7441235c4b91f878ca490a GIT binary patch literal 925 zcmZ?wbhEHb6krfwXlDQcOG`@zo00>EP98sb?%Iv}x9&W=d;iIUM=u^ffBoXs``2$j zefaeC^Vc8WzW)L%A0EP98sb?%Iv}x9&W=d;iIUM=u^ffBoXs``2$j zefaeC^Vc8WzW)L%A0OPUze{FlkhSdhvvU0?W!kAXm=?`#Rf zds{3&G@NI)EdFO>aG~Mi0>@q{iHHR&gM0ku^2s=C)jGB+lwIh?DN*I{by54f{z{d- QiC|>nT_yWNL4m;<0CAvI*Z=?k literal 0 HcmV?d00001 diff --git a/src/webui/static/multiselect/resources/up2.gif b/src/webui/static/multiselect/resources/up2.gif new file mode 100644 index 0000000000000000000000000000000000000000..431ddd43c9350b853ced3df99bce361fc1f3c6c6 GIT binary patch literal 920 zcmZ?wbhEHb6krfw_|5+_f9`Z{2x#_x_Uyk6t`}{`$qM_pjf6 z`ta%N=dVA$eg6ekK1z&+!0-tH#h)yU3=I4XIv_8B@&p4%DT6$RgvW*i$3{LOt(YAJ z3C-;S%5E|z7A!j2EzYiYxZst`C0U!aW;F( zsUwvM3mWHJmHsm^*wAosu~xsFti^(r!BhR_@~Jp%U3F?z=<=kyU7_0H>zG)iWUN0p JIxsL;0|0XsRo(yq literal 0 HcmV?d00001