skos_browser/commit
Move basic functionality from Amalgame
author | Michiel Hildebrand |
---|---|
Wed Nov 7 17:46:42 2012 +0100 | |
committer | Michiel Hildebrand |
Wed Nov 7 17:46:42 2012 +0100 | |
commit | be10e9e003b5ecfcf3a003553bd3121e27cd90b9 |
tree | edecb3505525b93016a7793022526ed4aa4efcd9 |
parent | 922a922c743061e669c96626604387414b95bda8 |
Diff style: patch stat
diff --git a/api/skos_concepts.pl b/api/skos_concepts.pl new file mode 100644 index 0000000..7202f4a --- /dev/null +++ b/api/skos_concepts.pl @@ -0,0 +1,355 @@ +:- module(skos_concepts, + []). + +:- use_module(library(http/http_dispatch)). +:- use_module(library(http/http_parameters)). +:- use_module(library(http/html_write)). +:- use_module(library(http/http_json)). +:- use_module(library(http/json)). +:- use_module(library(http/json_convert)). +:- use_module(library(semweb/rdf_db)). +:- use_module(library(semweb/rdf_label)). + + +:- http_handler(skosapi(conceptschemes), http_conceptschemes, []). +:- http_handler(skosapi(concepts), http_concepts, []). + +%% http_concept_schemes(+Request) +% +% API handler to fetch concept schemes + +http_conceptschemes(Request) :- + http_parameters(Request, + [ offset(Offset, + [integer, default(0), + description('Start of the results returned')]), + limit(Limit, + [integer, default(20), + description('maximum number of results returned')]), + query(Query, + [optional(true), + description('keyword query to filter the results by')]) + ]), + ConceptScheme = concept(Concept, Label, true), + findall(ConceptScheme, concept_scheme(Query, Concept, Label), Cs), + length(Cs, Total), + list_offset(Cs, Offset, OffsetResults), + list_limit(OffsetResults, Limit, LimitResults, _), + prolog_to_json(LimitResults, JSONResults), + reply_json(json([offset=Offset, + limit=Limit, + totalNumberOfResults=Total, + results=JSONResults])). + +:- json_object + concept(id:atom, label:atom, hasNext:boolean). + +concept_scheme(Query, C, Label) :- + var(Query), + !, + rdf(C, rdf:type, skos:'ConceptScheme'), + rdf_display_label(C, Label). +concept_scheme(Query, C, Label) :- + rdf(C, rdf:type, skos:'ConceptScheme'), + once(label_prefix(Query, C, Lit)), + literal_text(Lit, Label). + + +%% http_concepts(+Request) +% +% API handler to fetch top concepts + +http_concepts(Request) :- + http_parameters(Request, + [ parent(Parent, + [description('Concept or concept scheme from which we request the concepts')]), + type(Type, + [oneof(topconcept,inscheme,child,descendant,related), + default(inscheme), + description('Method to determine the concepts')]), + offset(Offset, + [integer, default(0), + description('Start of the results returned')]), + limit(Limit, + [integer, default(20), + description('maximum number of results returned')]), + query(Query, + [optional(true), + description('keyword query to filter the results by')]) + ]), + C = concept(Concept, Label, HasNarrower), + findall(C, concept(Type, Parent, Query, Concept, Label, HasNarrower), Cs0), + sort(Cs0, Cs), + term_sort_by_arg(Cs, 2, Sorted), + length(Sorted, Total), + list_offset(Sorted, Offset, OffsetResults), + list_limit(OffsetResults, Limit, LimitResults, _), + prolog_to_json(LimitResults, JSONResults), + reply_json(json([parent=Parent, + offset=Offset, + limit=Limit, + totalNumberOfResults=Total, + results=JSONResults])). + +concept(Type, Parent, Query, Concept, Label, HasNarrower) :- + var(Query), + !, + concept_(Type, Parent, Concept), + has_narrower(Concept, HasNarrower), + rdf_display_label(Concept, Label). +concept(Type, Parent, Query, Concept, Label, HasNarrower) :- + concept_(Type, Parent, Concept), + once(label_prefix(Query, Concept, Lit)), + literal_text(Lit, Label), + has_narrower(Concept, HasNarrower). + +concept_(inscheme, ConceptScheme, Concept) :- !, + inscheme(ConceptScheme, Concept). +concept_(topconcept, ConceptScheme, Concept) :- !, + top_concept(ConceptScheme, Concept). +concept_(child, Parent, Concept) :- + narrower_concept(Parent, Concept). +concept_(descendant, Parent, Concept) :- + descendant(Parent, Concept). +concept_(related, Parent, Concept) :- + related_concept(Parent, Concept). + +%% inscheme(+ConceptScheme, -Concept) +% +% True if Concept is contained in a skos:ConceptScheme by +% skos:inScheme. + +inscheme(ConceptScheme, Concept) :- + rdf(Concept, skos:inScheme, ConceptScheme). + +%% top_concept(+ConceptScheme, -Concept) +% +% True if Concept is a skos:hasTopConcept of ConceptScheme, or +% inversely by skos:topConceptOf + +top_concept(ConceptScheme, Concept) :- + rdf(ConceptScheme, skos:hasTopConcept, Concept). +top_concept(ConceptScheme, Concept) :- + rdf(Concept, skos:topConceptOf, ConceptScheme), + \+ rdf(ConceptScheme, skos:hasTopConcept, Concept). + +%% narrower_concept(+Concept, -Narrower) +% +% True if Narrower is related to Concept by skos:narrower or +% inversely by skos:broader. + +narrower_concept(Concept, Narrower) :- + rdf_has(Narrower, skos:broader, Concept). +narrower_concept(Concept, Narrower) :- + rdf_has(Concept, skos:narrower, Narrower), + \+ rdf_has(Narrower, skos:narrower, Concept). + +%% descendant(+Concept, -Descendant) +% +% Descendant is a child of Concept or recursively of its children + +descendant(Concept, Descendant) :- + narrower_concept(Concept, Narrower), + ( Descendant = Narrower + ; descendant(Narrower, Descendant) + ). + +%% related_concept(+Concept, -Related) +% +% True if Related is related to Concept by skos:related. + +related_concept(Concept, Related) :- + rdf_has(Concept, skos:related, Related). +related_concept(Concept, Related) :- + rdf_has(Related, skos:related, Concept), + \+ rdf_has(Concept, skos:related, Related). + +%% has_narrower(+Concept, -Boolean) +% +% Boolean is true when concept has a skos:narrower concept. + +has_narrower(Concept, true) :- + rdf_has(Concept, skos:narrower, _), + !. +has_narrower(Concept, true) :- + rdf_has(_, skos:broader, Concept), + !. +has_narrower(_, false). + + +%% http_concept_info(+Request) +% +% API handler to fetch info about a URI. +% +% @TBD support for language tags + +http_concept_info(Request) :- + http_parameters(Request, + [ concept(C, + [description('Concept to request info about')]) + ]), + rdf_display_label(C, Label), + skos_description(C, Desc), + skos_alt_labels(C, AltLabels0), + delete(AltLabels0, Label, AltLabels), + skos_related_concepts(C, Related), + voc_get_computed_props(C, Amalgame), + format('Content-type: text/html~n~n'), + phrase(html(\html_info_snippet(C, Label, Desc, AltLabels, Related, Amalgame)), HTML), + print_html(HTML). + +skos_description(C, Desc) :- + ( rdf_has(C, skos:scopeNote, Lit) + -> literal_text(Lit, Desc) + ; Desc = '' + ). +skos_alt_labels(C, AltLabels) :- + findall(AL, ( rdf_has(C, skos:altLabel, Lit), + literal_text(Lit, AL) + ), + AltLabels0), + sort(AltLabels0, AltLabels). +skos_related_concepts(C, Related) :- + Concept = concept(R, Label), + findall(Concept, ( skos_related(C, R), + rdf_display_label(R, Label) + ), + Related). + +skos_related(C, R) :- + rdf_has(C, skos:related, R). +skos_related(C, R) :- + rdf_has(R, skos:related, C), + \+ rdf_has(C, skos:related, R). + +html_info_snippet(URI, Label, Desc, AltLabels, Related, AmalGame) --> + { truncate_atom(Desc, 80, ShortDesc) + }, + html(div(class(infobox), + [ h3([\resource_link(URI, Label), + \html_label_list(AltLabels) + ]), + div(class(uri), URI), + div(style('float:left'), + [ div([class(desc), title(Desc)], ShortDesc), + \html_related_list(Related) + ]), + div(class(amalgame), + \html_amalgame_props(AmalGame)) + ])). + +html_label_list([]) --> !. +html_label_list(Ls) --> + html(span(class(altlabels), + [ ' (', + \html_label_list_(Ls), + ')' + ])). + +html_label_list_([L]) --> !, + html(span(class(label), L)). +html_label_list_([L|Ls]) --> + html(span(class(label), [L,', '])), + html_label_list_(Ls). + +html_related_list([]) --> !. +html_related_list(Cs) --> + html(div(class(related), + [ 'related: ', + \html_concept_list(Cs) + ])). + +html_concept_list([concept(URI, Label)]) --> !, + resource_link(URI, Label). +html_concept_list([concept(URI, Label)|Cs]) --> + html([\resource_link(URI, Label), ', ']), + html_concept_list(Cs). + +html_amalgame_props([]) --> !. +html_amalgame_props(Props) --> + html(table(\amalgame_props(Props))). + +amalgame_props([]) --> !. +amalgame_props([Term|Ts]) --> + { Term =.. [Prop, Value], + literal_text(Value, Txt) + }, + html(tr([td(Prop), td(Txt)])), + amalgame_props(Ts). + +resource_link(URI, Label) --> + { www_form_encode(URI, EncURI) + }, + html(a(href(location_by_id(list_resource)+'?r='+EncURI), Label)). + + /******************************* + * UTILILIES * + *******************************/ + +%% terms_sort_by_arg(+ListOfTerms, +N, -SortedTerms) +% +% Sorts ListOfTerms by the nth argument of each term. + +term_sort_by_arg(List, Arg, Sorted) :- + maplist(arg_key(Arg), List, Pairs), + keysort(Pairs, Sorted0), + pairs_values(Sorted0, Sorted). + +arg_key(N, Term, Key-Term) :- + arg(N, Term, Key). + +%% list_offset(+List, +N, -SmallerList) +% +% SmallerList starts at the nth element of List. + +list_offset(L, N, []) :- + length(L, Length), + Length < N, + !. +list_offset(L, N, L1) :- + list_offset_(L, N, L1). + +list_offset_(L, 0, L) :- !. +list_offset_([_|T], N, Rest) :- + N1 is N-1, + list_offset_(T, N1, Rest). + +%% list_limit(+List, +N, -SmallerList, -Rest) +% +% SmallerList ends at the nth element of List. + +list_limit(L, N, L, []) :- + length(L, Length), + Length < N, + !. +list_limit(L, N, L1, Rest) :- + list_limit_(L, N, L1, Rest). + +list_limit_(Rest, 0, [], Rest) :- !. +list_limit_([H|T], N, [H|T1], Rest) :- + N1 is N-1, + list_limit_(T, N1, T1, Rest). + +%% label_prefix(+Query, -R, -Lit) +% +% True if Query matches a literal value of R. + +label_prefix(Query, R, Lit) :- + rdf_has(R, rdfs:label, literal(prefix(Query), Lit)). +label_prefix(Query, R, Lit) :- + rdf_has(O, rdf:value, literal(prefix(Query), Lit)), + rdf_has(R, rdfs:label, O). + + +%% reply_jsonp(+JSON, +Callback) +% +% Output an html script node, where JSON is embedded in a +% javascript funtion. + +reply_jsonp(JSON, Callback) :- + with_output_to(string(JSONString), + json_write(current_output, JSON, [])), + format('Content-type: text/javascript~n~n'), + phrase(html([Callback,'(',JSONString,')']), HTML), + print_html(HTML). + diff --git a/applications/skos_browser.pl b/applications/skos_browser.pl new file mode 100644 index 0000000..66a1143 --- /dev/null +++ b/applications/skos_browser.pl @@ -0,0 +1,98 @@ +:- module(skos_browser, []). + +:- use_module(library(http/http_dispatch)). +:- use_module(library(http/html_write)). +:- use_module(library(http/http_path)). +:- use_module(library(http/html_head)). +:- use_module(library(yui3_beta)). + +:- http_handler(skosbrowser(.), http_skos_browser, []). + + +%% http_skos_browser(+Request) +% +% HTTP handler for web page that instantiates a JavaScript +% column browser widget. It uses the skos apis defined in +% @skos_concepts + +http_skos_browser(_Request) :- + reply_html_page(cliopatria(default), + [ title(['SKOS vocabulary browser']) + ], + [ \html_requires(css('columnbrowser.css')), + h2('SKOS vocabulary browser'), + div([class('yui-skin-sam'), id(browser)], []), + script(type('text/javascript'), + [ \yui_script + ]) + ]). + +%% yui_script(+Graph) +% +% Emit YUI object. + +yui_script --> + { findall(M-C, js_module(M,C), Modules), + pairs_keys(Modules, Includes), + DS = datasource, + Browser = browser + }, + yui3([json([modules(json(Modules))])], + [datasource|Includes], + [ \skos_api_datasource(DS), + \skos_browser(DS, Browser) + ]). + +skos_api_datasource(DS) --> + yui3_new(DS, + 'Y.DataSource.IO', + {source:''} + ), + yui3_plug(DS, + 'Y.Plugin.DataSourceJSONSchema', + {schema: {resultListLocator: results, + resultFields: [id, label, hasNext, matches, scheme], + metaFields: {totalNumberOfResults:totalNumberOfResults} + } + }), + yui3_plug(DS, + 'Y.Plugin.DataSourceCache', + {max:10}). + +skos_browser(DS, Browser) --> + { http_location_by_id(http_conceptschemes, ConceptSchemes), + http_location_by_id(http_concepts, Concepts) + }, + yui3_new(Browser, + 'Y.mazzle.ColumnBrowser', + {datasource: symbol(DS), + maxNumberItems: 100, + columns: [ + { request: ConceptSchemes, + label: 'concept scheme' + }, + { request: Concepts, + label: concept, + params: {type:'topconcept'}, + options: [ + {value:inscheme, label:'all concepts'}, + {value:topconcept, selected:true, label: 'top concepts'} + ] + }, + { request: Concepts, + params: {type:child}, + options: [], + repeat: true + } + ] + }), + yui3_render(Browser, id(browser)). + +js_module(resourcelist, json([fullpath(Path), + requires([node,event,widget]) + ])) :- + http_absolute_location(js('resourcelist.js'), Path, []). +js_module(columnbrowser, json([fullpath(Path), + requires([node,event,widget,resourcelist]) + ])) :- + http_absolute_location(js('columnbrowser.js'), Path, []). diff --git a/config-available/skos_browser.pl b/config-available/skos_browser.pl index ae6bbd8..d71a446 100644 --- a/config-available/skos_browser.pl +++ b/config-available/skos_browser.pl @@ -1,5 +1,15 @@ :- module(conf_skos_browser, []). +:- use_module(library(semweb/rdf_library)). +:- use_module(library(skos_schema)). + /** <module> SKOS vocabulary browser */ +http:location(skosbrowser, cliopatria(skos/browser), []). +http:location(skosapi, cliopatria(skos/api), []). + +:- use_module(api(skos_concepts)). +:- use_module(applications(skos_browser)). + +:- rdf_attach_library(vocs). diff --git a/web/css/columnbrowser.css b/web/css/columnbrowser.css new file mode 100644 index 0000000..24eba36 --- /dev/null +++ b/web/css/columnbrowser.css @@ -0,0 +1,81 @@ +.yui3-columnbrowser { + border-width: 1px; + border-style: solid; + border-color: #CCC; + + font-size: 12px; +} +.yui3-columnbrowser .hidden { + display: none; +} +.yui3-columnbrowser .loading { + padding-top: 20px; + background: url(loading.gif) no-repeat center center +} +.yui3-columnbrowser .columns-box { + height: 300px; + overflow-x: auto; + overflow-y: hidden; +} +.yui3-columnbrowser .columns-box.noscroll { + overflow-x: hidden; +} +.yui3-columnbrowser .columns { + height: 100%; +} + +.yui3-columnbrowser .columns .yui3-resourcelist { + float: left; + height: 100%; + overflow-x: auto; + overflow-y: scroll; +} +.yui3-columnbrowser .yui3-resourcelist-content ul { + padding: 2px 0; + margin: 0; +} +.yui3-columnbrowser .resourcelist-item { + padding: 3px 5px 2px; + cursor: pointer; + overflow: hidden; +} +.yui3-columnbrowser .resourcelist-item .more { + float: right; +} +.yui3-columnbrowser .resourcelist-item.selected { + background-color: #DDD; +} +.yui3-columnbrowser .yui3-resourcelist.selected .resourcelist-item.selected { + background-color: #3875D7; + color: #FFF; +} +.yui3-columnbrowser .pagination { + border-color:#CCCCCC; + border-style:solid; + border-width:1px 0 0; + text-align:center; +} +.yui3-columnbrowser .pagination .page-label { + padding: 0 10px; +} +.yui3-columnbrowser .pagination a.disabled { + color: #CCC; +} + +.yui3-columnbrowser .search, +.yui3-columnbrowser select { + border-color:#CCC; + border-style:solid; + border-width:0 0 1px; +} +.yui3-columnbrowser .search { + padding: 0 2px +} +.yui3-columnbrowser select { + width: 100%; +} +.yui3-columnbrowser .search input { + width: 100%; + border: none; + background: url("search_bg_enabled.png") no-repeat right 60% #FFFFFF; +} \ No newline at end of file diff --git a/web/css/loading.gif b/web/css/loading.gif new file mode 100644 index 0000000..d324d7e Binary files /dev/null and b/web/css/loading.gif differ diff --git a/web/css/search_bg_enabled.png b/web/css/search_bg_enabled.png new file mode 100644 index 0000000..717691e Binary files /dev/null and b/web/css/search_bg_enabled.png differ diff --git a/web/js/columnbrowser.js b/web/js/columnbrowser.js new file mode 100644 index 0000000..986cc59 --- /dev/null +++ b/web/js/columnbrowser.js @@ -0,0 +1,249 @@ +YUI.add('columnbrowser', function(Y) { + + var Lang = Y.Lang, + Widget = Y.Widget, + Node = Y.Node; + + Widget.ColumnBrowser = ColumnBrowser; + var NS = Y.namespace('mazzle'); + NS.ColumnBrowser = ColumnBrowser; + + /* ColumnBrowser class constructor */ + function ColumnBrowser(config) { + ColumnBrowser.superclass.constructor.apply(this, arguments); + } + + /* + * Required NAME static field, to identify the Widget class and + * used as an event prefix, to generate class names etc. (set to the + * class name in camel case). + */ + ColumnBrowser.NAME = "columnbrowser"; + + /* + * The attribute configuration for the ColumnBrowser widget. Attributes can be + * defined with default values, get/set functions and validator functions + * as with any other class extending Base. + */ + ColumnBrowser.ATTRS = { + datasource: { + value: null + }, + columns: { + value: null + }, + columnWidth: { + value: 200, + validator: Lang.isNumber + }, + maxNumberItems: { + value: 100, + validator: Lang.isNumber + }, + minQueryLength: { + value: 2, + validator: Lang.isNumber + }, + queryDelay: { + value: 0.3, + validator: Lang.isNumber + }, + selected: { + value: null + } + }; + + /* ColumnBrowser extends the base Widget class */ + Y.extend(ColumnBrowser, Widget, { + + initializer: function(config) { + // internal variables + this._activeIndex = null; + this._selectedIndex = null; + + // internal references to nodes + this.columnsBox = null; + this.columnsNode = null; + + this.publish("itemSelect", {}); + }, + + destructor : function() { + // purge itemSelect, optionSelect, offSetSelect, valueChange? + }, + + renderUI : function() { + this.columnsBox = this.get("contentBox") + .appendChild(Node.create('<div class="columns-box"></div>')); + this.columnsNode = this.columnsBox + .appendChild(Node.create('<div class="columns"></div>')); + }, + + bindUI : function() { + }, + + syncUI : function() { + if(this._activeIndex===0||this._activeIndex) { + var columns = this.get("columns"), + activeIndex = this._activeIndex, + selectedIndex = this._selectedIndex, + selectedItem = this._selectedItem; + + // update the status of the columns + // to "selected" or "hidden" + for (var i=0; i < columns.length; i++) { + var list = columns[i].list; + if(list) { + if(i==selectedIndex) { + list.get("boundingBox").addClass("selected"); + } else { + list.get("boundingBox").removeClass("selected"); + } + if(i<=activeIndex) { + list.set("visible", true); + } else { + list.set("visible", false); + } + } + } + + // update the active column + this._updateContentSize(); + //columns[activeIndex].list._node.scrollIntoView(); + } else { + this._activeIndex = 0; + this._updateColumn(0); + } + }, + + /** + * Public functions to fetch ids and labels from result items + */ + itemId : function(item) { + var id = item.id ? item.id : item; + return id; + }, + itemLabel : function(item) { + var label = item.label ? item.label : item; + return label; + }, + + /** + * Handles the selection of a resource list item. + * Fires the itemSelect event + * + * @private + * @param listItem {Object} the list element node + * @param resource {Object} the selected resource + **/ + _itemSelect : function(listItem, oItem, index) { + var columns = this.get("columns"), + next = index+1; + + this.set("selected", oItem); + this._selectedItem = oItem; + this._selectedIndex = index; + + if(columns[next]||columns[index].repeat) { + if(oItem.hasNext) { + this._updateColumn(next, this.itemId(oItem)); + } else { + this.syncUI(); + } + } + + this.fire("itemSelect", oItem, index, listItem); + }, + _setActiveColumn : function(e, index) { + this._selectedIndex = index>=1 ? index-1 : null; + this._activeIndex = index; + this.syncUI(); + }, + + /** + * Creates a new column based on Y.mazzle.ResourceList + * + * @private + **/ + _updateColumn : function(index, parent) { + if(!this.get("columns")[index]) { + this.get("columns")[index] = {}; + } + + var column = this.get("columns")[index]; + if(!column.list) { + column.list = this._createColumnList(index); + } + if(parent) { + column.list.set("params.parent", parent); + } + column.list.updateContent(); + }, + + _createColumnList : function(index) { + var columns = this.get("columns"), + previous = columns[index-1]||{}, + column = columns[index]; + + var cfg = { + width: this.get("columnWidth"), + maxNumberItems: this.get("maxNumberItems"), + minQueryLength: this.get("minQueryLength"), + queryDelay: this.get("queryDelay") + }; + + column.repeat = column.repeat||previous.repeat; + column.label = column.label || (column.repeat&&previous.label); + + // column properties are defined or inherited from previous column + cfg.datasource = this.get("datasource"); + cfg.request = column.request||(column.repeat&&previous.request); + cfg.params = column.params||(column.repeat&&previous.params); + cfg.options = column.options||(column.repeat&&previous.options); + + var list = new Y.mazzle.ResourceList(cfg); + if(column.formatter) { + list.formatItem = column.formatter + } + list.render(this.columnsNode); + + + list.on("itemClick", this._itemSelect, this, index); + list.on("beforeContentUpdate", this._setActiveColumn, this, index); + return list; + }, + + /** + * Handles resizing column content by + * setting the size of this.colomnsNode to the width of the combined columns + **/ + _updateContentSize : function() { + var columns = this.get("columns"), + content = this.columnsNode, + width = 0; + + for (var i=0; i < columns.length; i++) { + var columnList = columns[i].list; + if(columnList&&columnList.get("visible")) { + width += columnList.get("boundingBox").get("offsetWidth"); + } + } + + content.setStyle("width", width+"px"); + content.get("parentNode").removeClass("noscroll"); + this._updateColumnsSize(); + }, + + _updateColumnsSize : function() { + var columns = this.get("columns"), + height = this.columnsNode.get("offsetHeight"); + for (var i=0; i < columns.length; i++) { + if(columns[i].list) { + columns[i].list.set("height", height+"px"); + } + } + } + + }); + +}, 'gallery-2010.03.02-18' ,{requires:['node','event','widget','resourcelist','value-change']}); \ No newline at end of file diff --git a/web/js/resourcelist.js b/web/js/resourcelist.js new file mode 100644 index 0000000..ba6c217 --- /dev/null +++ b/web/js/resourcelist.js @@ -0,0 +1,437 @@ +YUI.add('resourcelist', function(Y) { + + var Lang = Y.Lang, + Widget = Y.Widget, + Node = Y.Node; + + Widget.ResourceList = ResourceList; + var NS = Y.namespace('mazzle'); + NS.ResourceList = ResourceList; + + /* ResourceList class constructor */ + function ResourceList(config) { + ResourceList.superclass.constructor.apply(this, arguments); + } + + /* + * Required NAME static field, to identify the Widget class and + * used as an event prefix, to generate class names etc. (set to the + * class name in camel case). + */ + ResourceList.NAME = "resourcelist"; + + /* + * The attribute configuration for the ResourceList widget. Attributes can be + * defined with default values, get/set functions and validator functions + * as with any other class extending Base. + */ + ResourceList.ATTRS = { + query: { + value: null + }, + minQueryLength: { + value: 2, + validator: Lang.isNumber + }, + queryDelay: { + value: 0.3, + validator: Lang.isNumber + }, + page: { + value: 0, + validator: Lang.isNumber + }, + maxNumberItems: { + value: 100, + validator : Lang.isNumber + }, + totalNumberOfResults: { + value: 0, + validator: Lang.isNumber + }, + resources: { + value: [], + validator : Lang.isArray + }, + selected: { + value: [], + validator : Lang.isArray + }, + options: { + value: [], + validator: Lang.isArray + }, + loading: { + value: false, + validator: Lang.isBoolean + }, + datasource: { + value: null + }, + request: { + value: "" + }, + params: { + value: {} + } + }; + + /* Static constants used to define the markup templates used to create ResourceList DOM elements */ + ResourceList.LIST_CLASS = ResourceList.NAME+'-list'; + ResourceList.ITEM_CLASS = ResourceList.NAME+'-item'; + ResourceList.LIST_TEMPLATE = '<ul class="'+ResourceList.LIST_CLASS+'"></ul>'; + ResourceList.ITEM_TEMPLATE = '<li class="'+ResourceList.ITEM_CLASS+'"></li>'; + + /* ResourceList extends the base Widget class */ + Y.extend(ResourceList, Widget, { + + initializer: function(config) { + this._nDelayID = -1; + this._listItems = []; + this._list = null; + this._load = null; + this._pagination = null; + + this.publish("itemClick", {}); + this.publish("beforeContentUpdate", {}); + this.publish("afterContentUpdate", {}); + this.publish("optionSelect", {}); + }, + + destructor : function() { + }, + + renderUI : function() { + this._renderSearch(); + this._renderOptions(); + this._renderLoad(); + this._renderList(); + }, + + bindUI : function() { + Y.delegate("click", this._itemSelect, this._list, "li", this); + }, + + syncUI : function() { + + if(this.get("loading")) { + // replace content with loading message + this._list.addClass("hidden"); + this._load.removeClass("hidden"); + } + else { + // update the column content + this.populateList(); + // hide loading message and show content + this._load.addClass("hidden"); + this._list.removeClass("hidden"); + } + }, + + _renderList : function() { + var content = this.get("contentBox"), + nListLength = this.get("maxNumberItems"); + + // add ul + var list = content.one("."+ResourceList.LIST_CLASS); + if(!list) { + list = content.appendChild(Node.create(ResourceList.LIST_TEMPLATE)); + } + // add list items + var listItems = []; + for (var i=0; i < nListLength; i++) { + var listItem = list.appendChild(Node.create(ResourceList.ITEM_TEMPLATE)); + listItem.setStyle("display", "block"); + listItem._node._nItemIndex = i; + listItems.push({el:listItem}); + } + this._list = list; + this._listItems = listItems; + }, + + /** + * Creates a HTML select list with options provided in the + * configuration for columns[index]. + * An eventhandler is added to the HTML select element which is + * handled by _optionSelect + * + * @private + **/ + _renderOptions : function() { + var options = this.get("options"); + + if(options&&options.length>0) { + var optionsNode = this.get("contentBox") + .appendChild(Node.create('<select class="options"></select>')); + + for (var i=0; i < options.length; i++) { + var option = options[i], + value = option.value, + label = option.label ? option.label : value, + selected = option.selected ? 'selected' : ''; + optionsNode.insert('<option value="'+value+'" '+selected+'>'+label+'</option>'); + } + optionsNode.on("change", this._optionSelect, this); + } + }, + + _renderSearch : function() { + var search = this.get("contentBox") + .appendChild(Node.create('<div class="search"></div>')) + .appendChild(Node.create('<input type="text" />')); + search.on("valuechange", this._valueChangeHandler, this); + }, + + /** + * Creates pagination in a column. + * An eventhandler is added to the prev and next buttons which is + * handled by _offsetSelect + * The pagination is stored in column._pagination, such that it is created + * only once. + * If pagination already exists we simply show it. + * + * @private + * @param length {Integer} the number of resources + **/ + _renderPagination : function() { + var length = this.get("totalNumberOfResults"), + limit = this.get("maxNumberItems"), + start = this.get("page")*limit, + end = start+Math.min(limit, length); + + if(length>limit) { + if(!this._pagination) { // create the pagination HTML + var pagination = this.get("contentBox") + .appendChild(Node.create('<div class="pagination"></div>')); + pagination.appendChild(Node.create('<a href="javascript:{}" class="page-prev">prev</a>')) + .on("click", this._offsetSelect, this, -1); + pagination.insert('<span class="page-label"></span>'); + pagination.appendChild(Node.create('<a href="javascript:{}" class="page-next">next</a>')) + .on("click", this._offsetSelect, this, 1); + this._pagination = pagination; + } else { // or show it + this._pagination.removeClass("hidden"); + } + + // now disable the inactive buttons + if(length<limit) { + Y.one(".page-next").addClass("disabled"); + Y.one(".page-prev").removeClass("disabled"); + } else if (start===0) { + Y.one(".page-prev").addClass("disabled", true); + Y.one(".page-next").removeClass("disabled"); + } else { + Y.one(".page-next").removeClass("disabled"); + Y.one(".page-prev").removeClass("disabled"); + } + // and add the right labels + Y.one(".page-label").set("innerHTML", start+' -- '+end); + } + else if(this._pagination) { + this._pagination.addClass("hidden"); + } + }, + + _renderLoad : function() { + this._load = this.get("contentBox").appendChild( + Y.Node.create('<div class="hidden loading"></div>')); + }, + + populateList : function() { + var listItems = this._listItems, + resources = this.get("resources"), + numberItems = Math.min(this.get("maxNumberItems"), resources.length); + + this.clearSelection(); + // add resources + var i = 0; + for (i; i < numberItems; i++) { + var oResource = resources[i], + HTML = this.formatItem(oResource), + oItem = listItems[i], + elItem = oItem.el; + + oItem.resource = oResource; + elItem.set("innerHTML", HTML); + elItem.setStyle("display", "block"); + } + + // clear remaining elements + for (i; i < listItems.length; i++) { + var oItem = listItems[i], + elItem = oItem.el; + if(elItem.getStyle("display")=="none") { + return; + } else { + elItem.innerHTML = []; + elItem.setStyle("display", "none"); + oItem.resource = null; + } + } + this.setSelection(); + this._renderPagination(); + }, + + setSelection : function() { + var selected = this.get("selected"); + for (var i=0; i < selected.length; i++) { + selected[i].addClass("selected"); + } + }, + clearSelection : function() { + var selected = this.get("selected"); + for (var i=0; i < selected.length; i++) { + selected[i].removeClass("selected"); + } + this.set("selected", []); + }, + + _itemSelect : function(e) { + var listItems = this._listItems, + itemNode = e.currentTarget, + oResource = listItems[itemNode.get("_nItemIndex")].resource; + + // @tbd add support for multiple selections + this.clearSelection(); + this.set("selected", [itemNode]); + this.setSelection(); + + itemNode.addClass("selected"); + this.fire("itemClick", itemNode, oResource); + }, + + /** + * Handles the selection of a column option. + * Fires the optionSelect event + * + * @private + * @param e {Object} the event object + **/ + _optionSelect : function(e) { + var optionValue = e.currentTarget.get("value"); + this.set("page", 0); + this.set("params.type", optionValue); + this.fire("optionSelect", optionValue); + this.updateContent(); + }, + + /** + * Handles the selection of a pagination action + * Fires the offsetSelect event + * + * @private + * @param e {Object} the event object + * @param direction {1 or -1} indicator for next (1) or prev (-1) + **/ + _offsetSelect : function(e, direction) { + this.set("page", this.get("page")+direction); + this.fire("offsetSelect", direction); + this.updateContent(); + }, + + /** + * The handler that listens to valueChange events and decides whether or not + * to kick off a new query. + * + * @param {Object} The event object + * @private + **/ + _valueChangeHandler : function(e) { + var oSelf = this, + query = e.newVal; + + // Clear previous timeout to prevent old searches to push through + if(oSelf._nDelayID != -1) { + clearTimeout(oSelf._nDelayID); + } + + this.set("page", 0); + if (!query || query.length < this.get("minQueryLength")) { + this.set("query", ""); + this.updateContent(); + } + else { + // Set a timeout to prevent too many search requests + var oSelf = this; + oSelf._nDelayID = setTimeout(function(){ + oSelf.set("query", query); + oSelf.updateContent(); + }, this.get("queryDelay")*1000); + } + }, + + /** + * Fetches data by doing a + * request on the datasource. + * + * @private + **/ + updateContent : function() { + this.fire("beforeContentUpdate"); + var request = this.get("request"), + params = this.get("params"); + + this._nDelayID = -1; // reset search query delay + this.set("loading", true); + this.syncUI(); + + // the request configuration attribute consist of params in + // the column definition and the current status of the column + params.limit = this.get("maxNumberItems"); + params.offset = this.get("page")*this.get("maxNumberItems"); + params.query = this.get("query"); + + request = Lang.isFunction(request) + ? request.call(this, params) + : request+"?"+this._requestParams(params); + + var oSelf = this; + this.get("datasource").sendRequest({ + request:request, + callback: { + success: function(o) { + var results = o.response.results, + total = o.response.meta + ? o.response.meta.totalNumberOfResults + : results.length; + oSelf.set("totalNumberOfResults", total) + oSelf.set("loading", false); + oSelf.set("resources", results); + oSelf.syncUI(); + oSelf.fire("afterContentUpdate"); + }, + failure: function(o) { + oSelf.set("totalNumberOfResults", 0) + oSelf.set("loading", false); + oSelf.set("resources", []); + oSelf.syncUI(); + oSelf.fire("afterContentUpdate"); + } + } + }); + }, + + _requestParams : function(params) { + var paramString = ""; + for(var key in params) { + if(params[key]) { + paramString += key+"="+encodeURIComponent(params[key])+"&"; + } + } + return paramString; + }, + + formatItem : function(oResource) { + var label = oResource["label"], + uri = oResource["id"], + value = (label&&!Y.Lang.isObject(label)) ? label : uri; + var HTML = ""; + if(oResource.hasNext) { HTML += "<div class='more'>></div>"; } + HTML += "<div class='resourcelist-item-value' title='"+uri+"'>"+value+"</div>"; + return HTML; + } + + + }); + +}, 'gallery-2010.03.02-18' ,{requires:['node','event','widget']}); \ No newline at end of file