diff --git a/frontend/package.json b/frontend/package.json index c1de56d7..0a934e3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "async": "^3.1.0", "backbone": "^1.4.0", "backbone-fractal": "^1.1.0", + "backbone-machina": "^1.1.0", "backbone.radio": "^2.0.0", "bulma": "^0.7.1", "bulma-accordion": "^2.0.1", @@ -28,7 +29,6 @@ "js-cookie": "^2.2.0", "jsonld": "^1.5.1", "lodash": "^4.17.10", - "machina": "^2.0.2", "rangy": "^1.3.0", "rdf-parse": "^1.1.1", "streamify-string": "^1.0.1" diff --git a/frontend/src/annotation/panel-annotation-edit-view-test.ts b/frontend/src/annotation/panel-annotation-edit-view-test.ts index fa75dec0..681db6c9 100644 --- a/frontend/src/annotation/panel-annotation-edit-view-test.ts +++ b/frontend/src/annotation/panel-annotation-edit-view-test.ts @@ -36,7 +36,6 @@ describe('AnnotationEditView', function() { range, positionDetails: this.positionDetails, source: new Node({'@id': 'x'}), - ontology: new Graph(), model: undefined, })).not.toThrow(); }); @@ -46,10 +45,7 @@ describe('AnnotationEditView', function() { const annotation = items.get(item('100')); const creator = annotation.get(dcterms.creator)[0] as Node; ldChannel.reply('current-user-uri', constant(creator.id)); - const view = new AnnotationEditView({ - ontology: new Graph(), - model: annotation, - }).render(); + const view = new AnnotationEditView({ model: annotation }).render(); expect(view.$('.panel-footer button.is-danger').length).toBe(1); view.remove(); ldChannel.stopReplying('current-user-uri'); @@ -59,10 +55,7 @@ describe('AnnotationEditView', function() { const items = new Graph(mockItems); const annotation = items.get(item('100')); const creator = annotation.get(dcterms.creator)[0] as Node; - const view = new AnnotationEditView({ - ontology: new Graph(), - model: annotation, - }).render(); + const view = new AnnotationEditView({ model: annotation }).render(); expect(view.$('.panel-footer button.is-danger').length).toBe(0); view.remove(); }); diff --git a/frontend/src/annotation/panel-annotation-edit-view.ts b/frontend/src/annotation/panel-annotation-edit-view.ts index 7a3195af..f7393a4c 100644 --- a/frontend/src/annotation/panel-annotation-edit-view.ts +++ b/frontend/src/annotation/panel-annotation-edit-view.ts @@ -9,11 +9,12 @@ import Graph from '../jsonld/graph'; import ItemEditor from '../panel-ld-item/ld-item-edit-view'; import PickerView from '../forms/base-picker-view'; +import FilteredCollection from '../utilities/filtered-collection'; import ItemGraph from '../utilities/item-graph'; import ClassPickerView from '../utilities/ontology-class-picker/ontology-class-picker-view'; import ItemMetadataView from '../utilities/item-metadata/item-metadata-view'; import SnippetView from '../utilities/snippet-view/snippet-view'; -import { isType } from '../utilities/utilities'; +import { isRdfsClass, isType } from '../utilities/utilities'; import { AnnotationPositionDetails, getTargetDetails @@ -22,12 +23,23 @@ import { composeAnnotation, getAnonymousTextQuoteSelector } from './../utilities/annotation/annotation-creation-utilities'; +import explorerChannel from '../explorer/radio'; +import { announceRoute } from '../explorer/utilities'; import BaseAnnotationView from './base-annotation-view'; import FlatCollection from './flat-annotation-collection'; import annotationEditTemplate from './panel-annotation-edit-template'; +/** + * Helper function in order to pass the right classes to the classPicker. + */ +function getOntologyClasses() { + const ontology = ldChannel.request('ontology:graph') || new Graph(); + return new FilteredCollection(ontology, isRdfsClass); +} + +const announce = announceRoute('item:edit', ['model', 'id']); export interface ViewOptions extends BaseOpt { /** @@ -35,7 +47,6 @@ export interface ViewOptions extends BaseOpt { * can be undefined if range and positionDetails are set (i.e. in case of a new annotation) */ model: Node; - ontology: Graph; collection?: FlatCollection; /** @@ -56,7 +67,6 @@ export interface ViewOptions extends BaseOpt { export default class AnnotationEditView extends BaseAnnotationView { collection: FlatCollection; - ontology: Graph; source: Node; range: Range; positionDetails: AnnotationPositionDetails; @@ -77,7 +87,6 @@ export default class AnnotationEditView extends BaseAnnotationView { } initialize(options: ViewOptions): this { - this.ontology = options.ontology; this.itemOptions = new ItemGraph(); this.itemPicker = new PickerView({collection: this.itemOptions}); this.itemPicker.on('change', this.selectItem, this); @@ -97,6 +106,7 @@ export default class AnnotationEditView extends BaseAnnotationView { this.listenTo(this, 'textQuoteSelector', this.processTextQuoteSelector); this.processModel(options.model); this.listenTo(this.model, 'change', this.processModel); + this.on('announceRoute', announce); } else { this.processTextQuoteSelector(this.model); @@ -144,7 +154,7 @@ export default class AnnotationEditView extends BaseAnnotationView { initClassPicker(): this { this.classPicker = new ClassPickerView({ - collection: this.ontology, + collection: getOntologyClasses(), preselection: this.preselection }); @@ -224,7 +234,7 @@ export default class AnnotationEditView extends BaseAnnotationView { const anno = results.annotation; this.collection.underlying.add(anno); const flat = this.collection.get(anno.id); - this.trigger('annotationEditView:saveNew', this, flat, results.items); + explorerChannel.trigger('annotationEditView:saveNew', this, flat, results.items); } ); } @@ -232,7 +242,7 @@ export default class AnnotationEditView extends BaseAnnotationView { submitOldAnnotation(newItem: boolean): void { this.selectedItem && this.model.set(oa.hasBody, this.selectedItem); this.model.save(); - this.trigger('annotationEditView:save', this, this.model, newItem); + explorerChannel.trigger('annotationEditView:save', this, this.model, newItem); } reset(): this { @@ -289,7 +299,7 @@ export default class AnnotationEditView extends BaseAnnotationView { onCancelClicked(event: JQueryEventObject): this { event.preventDefault(); this.reset(); - this.trigger('annotationEditView:close', this); + explorerChannel.trigger('annotationEditView:close', this); return this; } diff --git a/frontend/src/annotation/panel-annotation-list-view.ts b/frontend/src/annotation/panel-annotation-list-view.ts index 585e3934..39faefbd 100644 --- a/frontend/src/annotation/panel-annotation-list-view.ts +++ b/frontend/src/annotation/panel-annotation-list-view.ts @@ -4,11 +4,15 @@ import { CollectionView } from '../core/view'; import { getScrollTop, animatedScroll, ScrollType } from './../utilities/scrolling-utilities'; import ItemSummaryBlock from '../utilities/item-summary-block/item-summary-block-view'; import LoadingSpinnerView from '../utilities/loading-spinner/loading-spinner-view'; +import explorerChannel from '../explorer/radio'; +import { announceRoute } from '../explorer/utilities'; import FlatModel from './flat-annotation-model'; import FlatCollection from './flat-annotation-collection'; import annotationsTemplate from './panel-annotation-list-template'; +const announce = announceRoute('source:annotated', ['model', 'id']); + /** * Explorer panel that displays a list of annotations as ItemSummaryBlocks. * @@ -41,7 +45,7 @@ export default class AnnotationListView extends CollectionView { }); user.on('logout:success', () => authFsm.handle('logout')); -user.on('registration:success', () => registrationForm.success()); -user.on('registration:error', (response) => registrationForm.error(response)); -user.on('registration:invalid', (errors) => registrationForm.invalid(errors)); // When authorization fails, just return to the last unprivileged state. userFsm.on('enter:authorizationDenied', () => { @@ -74,21 +70,11 @@ userFsm.on('enter:requestAuthorization', (fsm, action) => { authFsm.handle('login'); }); -userFsm.on('enter:registering', () => { - authFsm.handle('register'); -}); - authFsm.on('enter:unauthenticated', () => userFsm.handle('denied')); authFsm.on('enter:attemptLogin', () => loginForm.render().$el.appendTo('body')); authFsm.on('exit:attemptLogin', () => loginForm.$el.detach()); authFsm.on('enter:authenticated', () => userFsm.handle('granted')); authFsm.on('exit:authenticated', () => userFsm.handle('logout')); -authFsm.on('enter:registering', () => { - registrationForm.render().$el.appendTo('body'); -}); -authFsm.on('exit:registering', () => { - registrationForm.$el.detach(); -}); loginForm.on('submit', credentials => user.login(credentials)); loginForm.on('cancel', () => authFsm.handle('loginCancel')); diff --git a/frontend/src/aspects/exploration.ts b/frontend/src/aspects/exploration.ts new file mode 100644 index 00000000..1d493215 --- /dev/null +++ b/frontend/src/aspects/exploration.ts @@ -0,0 +1,69 @@ +import { partial, isString } from 'lodash'; + +import channel from '../explorer/radio'; +import * as act from '../explorer/route-actions'; +import router from '../global/exploration-router'; +import mainRouter from '../global/main-router'; +import explorer from '../global/explorer-view'; +import controller from '../global/explorer-controller'; +import { ensureSources } from '../global/sources'; +import sourceListPanel from '../global/source-list-view'; + +const browserHistory = window.history; +const resetSourceList = () => explorer.reset(sourceListPanel); + +/** + * Common patterns for the explorer routes. + */ +function deepRoute(obtainAction, resetAction) { + return ([serial]) => explorer.scrollOrAction( + browserHistory.state, + () => resetAction(controller, obtainAction(serial)) + ); +} +const sourceRoute = partial(deepRoute, act.getSource); +const itemRoute = partial(deepRoute, act.getItem); + +mainRouter.on('route:explore', () => { + ensureSources(); + explorer.scrollOrAction(sourceListPanel.cid, resetSourceList); +}); + +router.on('route:source:bare', sourceRoute(act.sourceWithoutAnnotations)); +router.on('route:source:annotated', sourceRoute(act.sourceWithAnnotations)); +router.on('route:item', itemRoute(act.item)); +router.on('route:item:edit', itemRoute(act.itemInEditMode)); +router.on('route:item:related', itemRoute(act.itemWithRelations)); +router.on('route:item:related:edit', itemRoute(act.itemWithEditRelations)); +router.on('route:item:external', itemRoute(act.itemWithExternal)); +router.on('route:item:external:edit', itemRoute(act.itemWithEditExternal)); +router.on('route:item:annotations', itemRoute(act.itemWithOccurrences)); + +channel.on({ + 'sourceview:showAnnotations': controller.reopenSourceAnnotations, + 'sourceview:hideAnnotations': controller.unlistSourceAnnotations, + 'sourceview:textSelected': controller.selectText, + 'annotationList:showAnnotation': controller.openSourceAnnotation, + 'annotationList:hideAnnotation': controller.closeSourceAnnotation, + 'annotationEditView:saveNew': controller.saveNewAnnotation, + 'annotationEditView:save': controller.saveAnnotation, + 'annotationEditView:close': controller.closeEditAnnotation, + 'lditem:showRelated': controller.listRelated, + 'lditem:showAnnotations': controller.listItemAnnotations, + 'lditem:showExternal': controller.listExternal, + 'lditem:editAnnotation': controller.editAnnotation, + 'lditem:editItem': controller.notImplemented, + 'relItems:itemClick': controller.openRelated, + 'relItems:edit': controller.editRelated, + 'externalItems:edit': controller.editExternal, + 'externalItems:edit-close': controller.closeEditExternal, + 'relItems:edit-close': controller.closeEditRelated, + 'source-list:click': controller.pushSourcePair, + 'searchResultList:itemClicked': controller.openSearchResult, +}, controller); +channel.on('currentRoute', (route, panel) => { + router.navigate(route); + // Amend the state that Backbone.history just pushed with the cid of the + // panel. + browserHistory.replaceState(panel.cid, document.title); +}); diff --git a/frontend/src/aspects/navigation.ts b/frontend/src/aspects/navigation.ts new file mode 100644 index 00000000..0c97ee50 --- /dev/null +++ b/frontend/src/aspects/navigation.ts @@ -0,0 +1,49 @@ +import { history } from 'backbone'; + +import footerView from '../global/footer-view'; +import menuView from '../global/menu-view'; +import welcomeView from '../global/welcome-view'; +import feedbackView from '../global/feedback-view'; +import uploadSourceForm from '../global/upload-source-form'; +import categoryStyles from '../global/category-styles'; +import user from '../global/user'; +import mainRouter from '../global/main-router'; +import explorationRouter from '../global/exploration-router'; +import userFsm from '../global/user-fsm'; +import explorerView from '../global/explorer-view'; + +history.once('route', () => { + menuView.render().$el.appendTo('#header'); + footerView.render().$el.appendTo('.footer'); + categoryStyles.$el.appendTo('body'); + // 133 is the height of the footer (got this number by manually testing) + // Note that the same number needs to be the height of the 'push' class in + // main.sass. 555 is min-height. + const availableHeight = Math.max($(window).height() - 160, 555); + explorerView.setHeight(availableHeight).render(); + uploadSourceForm.setHeight(availableHeight); +}); + +mainRouter.on('route:home', () => mainRouter.navigate('search')); +mainRouter.on('route:search', () => userFsm.handle('search')); +mainRouter.on('route:upload', () => userFsm.handle('upload')); +mainRouter.on('route:explore', () => userFsm.handle('explore')); +mainRouter.on('route:leave', () => userFsm.handle('leave')); + +explorationRouter.on('route', () => userFsm.handle('explore')); + +userFsm.on('enter:searching', () => welcomeView.render().$el.appendTo('#main')); +userFsm.on('exit:searching', () => welcomeView.$el.detach()); +userFsm.on('enter:uploading', () => { + uploadSourceForm.render().$el.appendTo('#main'); +}); +userFsm.on('exit:uploading', () => { + uploadSourceForm.reset(); + uploadSourceForm.$el.detach(); +}); +userFsm.on('enter:exploring', () => explorerView.$el.appendTo('#main')); +userFsm.on('exit:exploring', () => explorerView.$el.detach()); + +menuView.on('feedback', () => feedbackView.render().$el.appendTo('body')); + +feedbackView.on('close', () => feedbackView.$el.detach()); diff --git a/frontend/src/aspects/readit.ts b/frontend/src/aspects/readit.ts deleted file mode 100644 index 4b1a20bb..00000000 --- a/frontend/src/aspects/readit.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { history, View } from 'backbone'; -import { parallel } from 'async'; -import footerView from '../global/footer-view'; -import menuView from '../global/menu-view'; -import welcomeView from '../global/welcome-view'; -import feedbackView from './../global/feedback-view'; -import ExplorerView from '../panel-explorer/explorer-view'; - -import user from './../global/user'; - -import Graph from './../jsonld/graph'; -import Node from './../jsonld/node'; -import { JsonLdObject } from './../jsonld/json'; -import { item, readit, rdf, vocab } from '../jsonld/ns'; - -import { getOntology, getSources } from './../utilities/utilities'; - -import CategoryColorView from './../utilities/category-colors/category-colors-view'; -import SourceView from './../panel-source/source-view'; - -import directionRouter from '../global/direction-router'; -import userFsm from '../global/user-fsm'; -import directionFsm from '../global/direction-fsm'; -import uploadSourceForm from './../global/upload-source-form'; -import registrationFormView from './../global/registration-view'; -import confirmRegistrationView from './../global/confirm-registration-view'; - -import { oa } from './../jsonld/ns'; - -import mockOntology from './../mock-data/mock-ontology'; -import mockItems from './../mock-data/mock-items'; -import mockSources from './../mock-data/mock-sources'; -import mockStaff from '../mock-data/mock-staff'; -import LdItemView from '../panel-ld-item/ld-item-view'; -import RelatedItemsView from '../panel-related-items/related-items-view'; -import SearchResultBaseItemView from '../search/search-results/search-result-base-view'; - -import SourceListView from '../panel-source-list/source-list-view'; - -let explorerView; - -history.once('route', () => { - menuView.render().$el.appendTo('#header'); - menuView.on('feedback', () => { feedbackView.render().$el.appendTo('body'); }); - feedbackView.on('close', () => feedbackView.$el.detach()); - footerView.render().$el.appendTo('.footer'); -}); - -directionRouter.on('route:register', () => { - userFsm.handle('register'); -}); - -directionRouter.on('route:confirm-registration', (key) => { - user.on('confirm-registration:success', () => confirmRegistrationView.success()); - user.on('confirm-registration:notfound', () => confirmRegistrationView.notFound()); - user.on('confirm-registration:error', (response) => confirmRegistrationView.error(response)); - confirmRegistrationView.processKey(key); - directionFsm.handle('confirm'); -}); - -directionFsm.on('enter:confirming', () => { - confirmRegistrationView.render().$el.appendTo('#main'); -}); - -directionFsm.on('exit:confirming', () => { - confirmRegistrationView.$el.detach(); -}); - -directionRouter.on('route:arrive', () => { - directionFsm.handle('arrive'); - userFsm.handle('arrive'); -}); - -userFsm.on('enter:arriving', () => { - welcomeView.render().$el.appendTo('#main'); -}); - -userFsm.on('exit:arriving', () => { - welcomeView.$el.detach(); -}); - -directionRouter.on('route:upload', () => { - userFsm.handle('upload'); -}); - -userFsm.on('enter:uploading', () => { - uploadSourceForm.setHeight(getViewportHeight()); - uploadSourceForm.render().$el.appendTo('#main'); -}); - -userFsm.on('exit:uploading', () => { - uploadSourceForm.reset(); - uploadSourceForm.$el.detach(); -}); - -directionRouter.on('route:explore', () => { - userFsm.handle('explore'); -}); - -userFsm.on('enter:exploring', () => { - welcomeView.$el.detach(); - initSourceList(); -}); - -userFsm.on('exit:exploring', () => { - if (explorerView) explorerView.$el.detach(); -}); - - -directionRouter.on('route:leave', () => { - userFsm.handle('leave'); -}); - -/** - * Get the heigth of the available vertical space. - * Compensates for menu and footer, and 555 is min-height. - */ -function getViewportHeight(): number { - let vh = $(window).height(); - // 133 is the height of the footer (got this number by manually testing) - // Note that the same number needs to be the height of the 'push' class in main.sass - return Math.max(vh - 160, 555); -} - -function initExplorer(first: SourceListView, ontology: Graph): ExplorerView { - explorerView = new ExplorerView({ first: first, ontology: ontology }); - explorerView.setHeight(getViewportHeight()); - explorerView.$el.appendTo('#main'); - return explorerView; -} - -function initSourceList() { - let sources = new Graph(); - let ontology = new Graph(); - let sourceListView = new SourceListView({ - collection: sources - }); - - let explorerView = initExplorer(sourceListView, ontology); - - parallel([getOntology, getSources], function (error, results) { - if (error) console.debug(error); - else { - ontology = results[0]; - sources = results[1]; - sourceListView.collection.reset(sources.models); - let ccView = new CategoryColorView({ collection: ontology}); - ccView.render().$el.appendTo('body'); - explorerView.ontology.reset(ontology.models); - explorerView.render(); - } - }); -} diff --git a/frontend/src/aspects/registration.ts b/frontend/src/aspects/registration.ts new file mode 100644 index 00000000..f471357f --- /dev/null +++ b/frontend/src/aspects/registration.ts @@ -0,0 +1,40 @@ +import registrationForm from '../global/registration-view'; +import confirmRegistrationView from '../global/confirm-registration-view'; +import user from '../global/user'; +import mainRouter from '../global/main-router'; +import authFsm from '../global/authentication-fsm'; +import userFsm from '../global/user-fsm'; + +mainRouter.on('route:register', () => authFsm.handle('register')); +mainRouter.on('route:confirm-registration', (key) => { + confirmRegistrationView.processKey(key); + userFsm.handle('confirm'); +}); + +authFsm.on('enter:registering', () => { + registrationForm.render().$el.appendTo('body'); +}); +authFsm.on('exit:registering', () => { + registrationForm.$el.detach(); +}); +userFsm.on('enter:confirming', () => { + confirmRegistrationView.render().$el.appendTo('#main'); +}); +userFsm.on('exit:confirming', () => confirmRegistrationView.$el.detach()); + +user.on('registration:success', () => registrationForm.success()); +user.on('registration:error', (response) => registrationForm.error(response)); +user.on('registration:invalid', (errors) => registrationForm.invalid(errors)); +user.on('confirm-registration:success', () => { + confirmRegistrationView.success() +}); +user.on('confirm-registration:notfound', () => { + confirmRegistrationView.notFound() +}); +user.on('confirm-registration:error', (response) => { + confirmRegistrationView.error(response) +}); + +registrationForm.on('register', details => user.register(details)); + +confirmRegistrationView.on('confirm', key => user.confirmRegistration(key)); diff --git a/frontend/src/core/fsm.ts b/frontend/src/core/fsm.ts index 5dbeb2c6..8f48cf25 100644 --- a/frontend/src/core/fsm.ts +++ b/frontend/src/core/fsm.ts @@ -1,17 +1,8 @@ -import { Fsm } from 'machina'; +import Fsm from 'backbone-machina'; /** * This is the base FSM class that all FSMs in the application * should derive from, either directly or indirectly. If you want to * apply a customization to all FSMs in the application, do it here. */ -export default Fsm.extend({ - constructor() { - let result = Fsm.apply(this, arguments); - this.on('transition', ({fromState, toState, action}) => { - this.emit(`exit:${fromState}`, this, action); - this.emit(`enter:${toState}`, this, action); - }); - return result; - }, -}); +export default Fsm; diff --git a/frontend/src/global/scroll-easings.ts b/frontend/src/core/scroll-easings.ts similarity index 100% rename from frontend/src/global/scroll-easings.ts rename to frontend/src/core/scroll-easings.ts diff --git a/frontend/src/panel-explorer/explorer-event-controller.ts b/frontend/src/explorer/explorer-event-controller.ts similarity index 56% rename from frontend/src/panel-explorer/explorer-event-controller.ts rename to frontend/src/explorer/explorer-event-controller.ts index c56bd651..22d2bc4c 100644 --- a/frontend/src/panel-explorer/explorer-event-controller.ts +++ b/frontend/src/explorer/explorer-event-controller.ts @@ -8,11 +8,11 @@ import Graph from '../jsonld/graph'; import SourceView from './../panel-source/source-view'; import AnnotationListView from '../annotation/panel-annotation-list-view'; -import AnnotationEditView from '../annotation/panel-annotation-edit-view'; +import AnnoEditView from '../annotation/panel-annotation-edit-view'; import RelatedItemsView from '../panel-related-items/related-items-view'; -import RelatedItemsEditView from '../panel-related-items/related-items-edit-view'; -import ExternalResourcesView from '../panel-external-resources/external-resources-view'; -import ExternalResourcesEditView from '../panel-external-resources/external-resources-edit-view'; +import RelatedEditView from '../panel-related-items/related-items-edit-view'; +import ExternalView from '../panel-external-resources/external-resources-view'; +import ExternalEditView from '../panel-external-resources/external-resources-edit-view'; import ItemGraph from '../utilities/item-graph'; import FlatModel from '../annotation/flat-annotation-model'; import FlatCollection from '../annotation/flat-annotation-collection'; @@ -23,7 +23,6 @@ import { isType, isOntologyClass, } from '../utilities/utilities'; -import { Collection } from 'backbone'; export default class ExplorerEventController { /** @@ -33,57 +32,49 @@ export default class ExplorerEventController { mapSourceAnnotationList: Map = new Map(); mapAnnotationListSource: Map = new Map(); - mapAnnotationEditSource: Map = new Map(); + mapAnnotationEditSource: Map = new Map(); mapAnnotationListAnnotationDetail: Map = new Map(); constructor(explorerView: ExplorerView) { this.explorerView = explorerView; } - /** - * Subcribes to the events fired by the panel. - * Contains a neat trick: will subscribe to all known events, most of which will never be - * fired by the panel. No panel fires all different events. - * @param panel The panel to listen to. - */ - subscribeToPanelEvents(panel: View): void { - panel.on({ - 'sourceview:showAnnotations': graph => defer(this.sourceViewShowAnnotations.bind(this), graph), - 'sourceview:hideAnnotations': this.sourceViewHideAnnotations, - 'sourceview:textSelected': this.sourceViewOnTextSelected, - 'annotationList:showAnnotation': this.openAnnotationPanel, - 'annotationList:hideAnnotation': this.closeAnnotationPanel, - 'annotationEditView:saveNew': this.annotationEditSaveNew, - 'annotationEditView:save': this.annotationEditSave, - 'annotationEditView:close': this.annotationEditClose, - 'lditem:showRelated': this.ldItemShowRelated, - 'lditem:showAnnotations': this.ldItemShowAnnotations, - 'lditem:showExternal': this.ldItemShowExternal, - 'lditem:editAnnotation': this.ldItemEditAnnotation, - 'lditem:editItem': this.notImplemented, - 'relItems:itemClick': this.relItemsItemClicked, - 'relItems:edit': this.relItemsEdit, - 'externalItems:edit': this.externalItemsEdit, - 'externalItems:edit-close': this.externalItemsEditClose, - 'relItems:edit-close': this.relItemsEditClose, - 'source-list:click': this.pushSourcePair, - 'searchResultList:itemClicked': this.searchResultListItemClicked, - }, this); + pushSource(basePanel: View, source: Node): SourceView { + const sourcePanel = createSourceView(source, true, true); + this.explorerView.popUntil(basePanel).push(sourcePanel); + return sourcePanel.activate(); } - pushSourcePair(basePanel: View, source: Node): [SourceView, AnnotationListView] { - const sourcePanel = createSourceView(source, true, true); + resetSource(source: Node, showHighlights: boolean): SourceView { + const sourcePanel = createSourceView(source, showHighlights, true); + this.explorerView.reset(sourcePanel); + return sourcePanel.activate(); + } + + listSourceAnnotations(sourcePanel: SourceView): AnnotationListView { const listPanel = new AnnotationListView({ + model: sourcePanel.model, collection: sourcePanel.collection, }); this.mapSourceAnnotationList.set(sourcePanel, listPanel); this.mapAnnotationListSource.set(listPanel, sourcePanel); - this.explorerView.popUntil(basePanel).push(sourcePanel).push(listPanel); - sourcePanel.activate(); + this.explorerView.push(listPanel); + return listPanel; + } + + pushSourcePair(basePanel: View, source: Node): [SourceView, AnnotationListView] { + const sourcePanel = this.pushSource(basePanel, source); + const listPanel = this.listSourceAnnotations(sourcePanel); return [sourcePanel, listPanel]; } - searchResultListItemClicked(searchResults: SearchResultListView, item: Node) { + resetSourcePair(source: Node): [SourceView, AnnotationListView] { + const sourcePanel = this.resetSource(source, true); + const listPanel = this.listSourceAnnotations(sourcePanel); + return [sourcePanel, listPanel]; + } + + openSearchResult(searchResults: SearchResultListView, item: Node) { if (isType(item, oa.Annotation)) { const specificResource = item.get(oa.hasTarget)[0] as Node; const source = specificResource.get(oa.hasSource)[0] as Node; @@ -95,48 +86,43 @@ export default class ExplorerEventController { } } - relItemsItemClicked(relView: RelatedItemsView, item: Node): this { - this.explorerView.popUntil(relView).push(new LdItemView({ - model: item, - })); - return this; + openRelated(relView: RelatedItemsView, item: Node): LdItemView { + const itemPanel = new LdItemView({ model: item }); + this.explorerView.popUntil(relView).push(itemPanel); + return itemPanel; } - relItemsEdit(relView: RelatedItemsView, item: Node): this { - const editView = new RelatedItemsEditView({ model: item }); + editRelated(relView: RelatedItemsView, item: Node): RelatedEditView { + const editView = new RelatedEditView({ model: item }); this.explorerView.overlay(editView, relView); - return this; + return editView; } - relItemsEditClose(editView: RelatedItemsEditView): this { + closeEditRelated(editView: RelatedEditView): void { this.explorerView.removeOverlay(editView); - return this; } - externalItemsEdit(exView: ExternalResourcesView): this { - const editView = new ExternalResourcesEditView({ model: exView.model }); + editExternal(exView: ExternalView): ExternalEditView { + const editView = new ExternalEditView({ model: exView.model }); this.explorerView.overlay(editView, exView); - return this; + return editView; } - externalItemsEditClose(editView: ExternalResourcesEditView): this { + closeEditExternal(editView: ExternalEditView): void { this.explorerView.removeOverlay(editView); - return this; } - ldItemShowRelated(view: LdItemView, item: Node): this { + listRelated(view: LdItemView, item: Node): RelatedItemsView { if (!item) { alert('no related items!'); return; } - - this.explorerView.popUntil(view).push(new RelatedItemsView({ - model: item, - })); - return this; + const listView = new RelatedItemsView({ model: item }); + this.explorerView.popUntil(view).push(listView); + return listView; } - ldItemShowAnnotations(view: LdItemView, item: Node): this { + listItemAnnotations(view: LdItemView, item: Node): void { if (!item) { alert('no linked annotations!'); return; @@ -146,7 +132,11 @@ export default class ExplorerEventController { let items = new ItemGraph(); items.query({ predicate: oa.hasBody, object: item }).then( function success() { - let resultView = new SearchResultListView({ collection: new Graph(items.models), selectable: false }); + let resultView = new SearchResultListView({ + model: item, + collection: new Graph(items.models), + selectable: false, + }); self.explorerView.push(resultView); }, function error(error) { @@ -154,38 +144,31 @@ export default class ExplorerEventController { } ); this.explorerView.popUntil(view); - - return this; } - ldItemShowExternal(view: LdItemView, item: Node): this { + listExternal(view: LdItemView, item: Node): ExternalView { if (!item) { alert('no external resources!'); return; } - this.explorerView.popUntil(view).push(new ExternalResourcesView({ - model: item, - })); - return this; + const listView = new ExternalView({ model: item }); + this.explorerView.popUntil(view).push(listView); + return listView; } - ldItemEditAnnotation(ldItemview: LdItemView, annotation: Node): this { - let annoEditView = new AnnotationEditView({ - ontology: this.explorerView.ontology, - model: annotation, - }); + editAnnotation(ldItemview: LdItemView, annotation: Node): AnnoEditView { + let annoEditView = new AnnoEditView({ model: annotation }); this.explorerView.overlay(annoEditView, ldItemview); - return this; + return annoEditView; } - annotationEditSave(editView: AnnotationEditView, annotation: Node, newItem: boolean): this { + saveAnnotation(editView: AnnoEditView, annotation: Node, newItem: boolean): void { this.explorerView.removeOverlay(editView); // TODO: re-enable the next line. // if (newItem) this.autoOpenRelationEditor(annotation); - return this; } - annotationEditSaveNew(editView: AnnotationEditView, annotation: FlatModel, created: ItemGraph): void { + saveNewAnnotation(editView: AnnoEditView, annotation: FlatModel, created: ItemGraph): void { const listView = editView['_listview']; if (listView) { this.explorerView.removeOverlay(editView); @@ -206,62 +189,58 @@ export default class ExplorerEventController { const item = newItems[0]; const relView = new RelatedItemsView({ model: item }); this.explorerView.push(relView); - this.relItemsEdit(relView, item); + this.editRelated(relView, item); } return this; } - annotationEditClose(editView: AnnotationEditView): this { + closeEditAnnotation(editView: AnnoEditView): void { let source = this.mapAnnotationEditSource.get(editView); let annoList = this.mapSourceAnnotationList.get(source); - if (annoList) this.explorerView.removeOverlay(editView); - else this.explorerView.pop(); - return this; - } - - annotationListEdit(view: AnnotationListView, annotationList): this { - this.notImplemented(); - return this; + if (annoList) { + this.explorerView.removeOverlay(editView); + } else { + this.explorerView.pop(); + } } - openAnnotationPanel(listView: AnnotationListView, anno: FlatModel): void { + openSourceAnnotation(listView: AnnotationListView, anno: FlatModel): void { const annoRDF = anno.get('annotation'); let newDetailView = new LdItemView({ model: annoRDF }); this.mapAnnotationListAnnotationDetail.set(listView, newDetailView); this.explorerView.popUntil(listView).push(newDetailView); } - closeAnnotationPanel(listView: AnnotationListView, annotation: FlatModel): void { + resetItem(item: Node): LdItemView { + let detailView = new LdItemView({ model: item }); + this.explorerView.reset(detailView); + return detailView; + } + + closeSourceAnnotation(listView: AnnotationListView, annotation: FlatModel): void { this.mapAnnotationListAnnotationDetail.delete(listView); this.explorerView.popUntil(listView); } - sourceViewShowAnnotations(sourceView: SourceView): this { - let annotationListView = new AnnotationListView({ - collection: sourceView.collection - }); + reopenSourceAnnotations(sourceView: SourceView): AnnotationListView { + const annoListView = this.listSourceAnnotations(sourceView); sourceView.collection.underlying.trigger('sync'); - this.mapSourceAnnotationList.set(sourceView, annotationListView); - this.mapAnnotationListSource.set(annotationListView, sourceView); - this.explorerView.push(annotationListView); - return this; + return annoListView; } - sourceViewHideAnnotations(sourceView): this { + unlistSourceAnnotations(sourceView): void { let annoListView = this.mapSourceAnnotationList.get(sourceView); this.mapSourceAnnotationList.delete(sourceView); this.mapAnnotationListSource.delete(annoListView); this.explorerView.popUntil(sourceView); - return this; } - sourceViewOnTextSelected(sourceView: SourceView, source: Node, range: Range, positionDetails: AnnotationPositionDetails): this { + selectText(sourceView: SourceView, source: Node, range: Range, positionDetails: AnnotationPositionDetails): AnnoEditView { let listView = this.mapSourceAnnotationList.get(sourceView); - let annoEditView = new AnnotationEditView({ + let annoEditView = new AnnoEditView({ range: range, positionDetails: positionDetails, source: source, - ontology: this.explorerView.ontology, model: undefined, collection: sourceView.collection, }); @@ -270,11 +249,10 @@ export default class ExplorerEventController { if (listView) { annoEditView['_listview'] = listView; this.explorerView.popUntil(listView).overlay(annoEditView); - } - else { + } else { this.explorerView.push(annoEditView); } - return this; + return annoEditView; } notImplemented() { diff --git a/frontend/src/panel-explorer/explorer-panelstack-view.ts b/frontend/src/explorer/explorer-panelstack-view.ts similarity index 100% rename from frontend/src/panel-explorer/explorer-panelstack-view.ts rename to frontend/src/explorer/explorer-panelstack-view.ts diff --git a/frontend/src/panel-explorer/explorer-view-test.ts b/frontend/src/explorer/explorer-view-test.ts similarity index 91% rename from frontend/src/panel-explorer/explorer-view-test.ts rename to frontend/src/explorer/explorer-view-test.ts index 32fcf14d..693c8b22 100644 --- a/frontend/src/panel-explorer/explorer-view-test.ts +++ b/frontend/src/explorer/explorer-view-test.ts @@ -1,15 +1,12 @@ import { $ } from 'backbone'; -import { times, after } from 'lodash'; +import { times, after, size } from 'lodash'; -import './../global/scroll-easings'; +import './../core/scroll-easings'; import { enableI18n } from '../test-util'; import ExplorerView from './explorer-view'; import View from './../core/view'; -import mockOntology from './../mock-data/mock-ontology'; -import Graph from './../jsonld/graph'; - import fastTimeout from '../utilities/fastTimeout'; describe('ExplorerView', function () { @@ -17,8 +14,7 @@ describe('ExplorerView', function () { beforeEach(function () { let firstPanel = new View(); - let ontology = new Graph(mockOntology); - this.view = new ExplorerView({ first: firstPanel, ontology: ontology }); + this.view = new ExplorerView({ first: firstPanel }); }); afterEach(function() { @@ -236,4 +232,17 @@ describe('ExplorerView', function () { this.view.popUntil(stack1Panel2); expectSame(); }); + + it('can reset the panels wholesale', function() { + this.view.push(new View()).push(new View()).push(new View()); + expect(this.view.stacks.length).toBe(4); + const replacement = new View(); + const spy = jasmine.createSpy('resetSpy'); + this.view.once('reset', spy); + this.view.reset(replacement); + expect(spy).toHaveBeenCalledWith(this.view); + expect(this.view.stacks.length).toBe(1); + expect(size(this.view.rltPanelStack)).toBe(1); + expect(this.view.rltPanelStack[replacement.cid]).toBe(0); + }); }); diff --git a/frontend/src/panel-explorer/explorer-view.ts b/frontend/src/explorer/explorer-view.ts similarity index 84% rename from frontend/src/panel-explorer/explorer-view.ts rename to frontend/src/explorer/explorer-view.ts index 5458f55e..d4b48979 100644 --- a/frontend/src/panel-explorer/explorer-view.ts +++ b/frontend/src/explorer/explorer-view.ts @@ -6,13 +6,12 @@ import { isFunction, sortedIndexBy, constant, + isString, } from 'lodash'; import Model from '../core/model'; import View from '../core/view'; -import Graph from '../jsonld/graph'; import PanelStackView from './explorer-panelstack-view'; -import EventController from './explorer-event-controller'; import { animatedScroll, ScrollType } from './../utilities/scrolling-utilities'; import fastTimeout from '../utilities/fastTimeout'; @@ -21,15 +20,11 @@ const scrollFudge = 100; export interface ViewOptions extends BaseOpt { // TODO: do we need a PanelBaseView? first: View; - ontology: Graph; } export default class ExplorerView extends View { - ontology: Graph; stacks: PanelStackView[]; - eventController: EventController; - /** * A reverse lookuptable containing each panel's cid as key, * and the position of the stack as the value. Perfect for finding @@ -44,11 +39,7 @@ export default class ExplorerView extends View { constructor(options?: ViewOptions) { super(options); - if (!options.ontology) throw new TypeError('ontology cannot be null or undefined'); - - this.ontology = options.ontology; this.stacks = []; - this.eventController = new EventController(this); this.rltPanelStack = {}; this.scroll = debounce(this.scroll, 100); this.push(options.first); @@ -66,24 +57,31 @@ export default class ExplorerView extends View { return this; } + has(cid: string): boolean { + return cid in this.rltPanelStack; + } + /** * Animated scroll to make a stack visible. * If the stack is not already visible, apply minimal horizontal scroll so * that the stack is just within the viewport. Otherwise, no animation * occurs. * By default scrolls to the rightmost stack. - * @param stack: Optional. The stack to focus on / scroll to. + * @param stack: Optional. The stack, or the cid of a panel, to + * focus on / scroll to. */ - scroll(stack?: PanelStackView, callback?: any): this { + scroll(stack?: string | PanelStackView, callback?: any): this { + if (isString(stack)) stack = this.stacks[this.rltPanelStack[stack]]; if (!stack) stack = this.getRightMostStack(); + stack.getTopPanel().trigger('announceRoute'); const thisLeft = this.$el.scrollLeft(); const thisRight = this.getMostRight(); const stackLeft = stack.getLeftBorderOffset(); - const stackRight = stack.getRightBorderOffset(); + const stackRight = stackLeft + stack.getWidth(); let scrollTarget; - if (stackRight - thisLeft < scrollFudge) { - scrollTarget = stackLeft; - } else if (thisRight - stackLeft < scrollFudge) { + if (stackRight - thisLeft < scrollFudge || + thisRight - stackLeft < scrollFudge + ) { scrollTarget = stackRight - $(window).width(); } else { if (callback) fastTimeout(callback); @@ -100,7 +98,6 @@ export default class ExplorerView extends View { * of the new panel's stack from the left. */ push(panel: View): this { - this.eventController.subscribeToPanelEvents(panel); let position = this.stacks.length; this.stacks.push(new PanelStackView({ first: panel })); let stack = this.stacks[position]; @@ -141,7 +138,6 @@ export default class ExplorerView extends View { stack.push(panel); this.rltPanelStack[panel.cid] = position; - this.eventController.subscribeToPanelEvents(panel); this.trigger('overlay', panel, ontoPanel, position, (position - this.stacks.length)); this.scroll(stack); return this; @@ -178,16 +174,18 @@ export default class ExplorerView extends View { removeOverlay(panel: View): View { // validate that the panel is on top of its stack let position = this.rltPanelStack[panel.cid]; - let stackTop = this.stacks[position].getTopPanel(); + const stack = this.stacks[position]; + let stackTop = stack.getTopPanel(); if (panel.cid !== stackTop.cid) { throw new RangeError(`panel with cid '${panel.cid}' is not a topmost panel`); } - if (this.stacks[position].hasOnlyOnePanel()) { + if (stack.hasOnlyOnePanel()) { throw new RangeError(`cannot remove panel with cid '${panel.cid}' because it is a bottom panel (not an overlay)`); } let removedPanel = this.deletePanel(position); - this.trigger('removeOverlay', removedPanel, this.stacks[position].getTopPanel(), position, (position - this.stacks.length)); + this.scroll(stack); + this.trigger('removeOverlay', removedPanel, stack.getTopPanel(), position, (position - this.stacks.length)); return removedPanel; } @@ -196,22 +194,39 @@ export default class ExplorerView extends View { * @param panel The panel that needs to become rightmost. */ popUntil(panel: View): this { - let i = 0; - while (this.getRightMostStack().getTopPanel().cid !== panel.cid && i < 1000) { - this.pop(); - i++; + if (this.rltPanelStack[panel.cid] == null) { + throw new RangeError('Cannot find panel to pop until.'); } - if (i === 999) { - // Note that this check exists only to protect developers. - // If one consumes `popUntil` without being aware it will async, - // `panel` might be replaced while the popping is not completed yet, - // resulting inan infinite loop. - throw new RangeError('Cannot find panel to pop until. Do you need to async?'); + while (this.getRightMostStack().getTopPanel().cid !== panel.cid) { + this.pop(); } this.trigger('pop:until', panel); return this; } + /** + * Remove all panels, then make `panel` the new first panel. + * @param panel The panel that needs to become leftmost. + */ + reset(panel: View): this { + while (this.stacks.length) this.pop(); + this.trigger('reset', this).push(panel); + return this; + } + + /** + * Scroll to a panel if present, otherwise execute an action of choice. + * Useful in routing. + */ + scrollOrAction(cid, action: () => void): this { + if (isString(cid) && this.has(cid)) { + this.scroll(cid); + } else { + action(); + } + return this; + } + /** * Remove the topmost panel from the stack at position. Returns the deleted panel. * @param position The indes of the stack to remove the panel from @@ -248,8 +263,9 @@ export default class ExplorerView extends View { /** * Dynamically set the height for the explorer, based on the viewport height. */ - setHeight(height: number): void { + setHeight(height: number): this { this.$el.css('height', height); + return this; } onScroll(): void { @@ -260,6 +276,7 @@ export default class ExplorerView extends View { let topPanel = mostRightFullyVisibleStack.getTopPanel(); let position = this.rltPanelStack[topPanel.cid]; this.trigger('scrollTo', topPanel, position, (position - this.stacks.length)); + topPanel.trigger('announceRoute'); } } diff --git a/frontend/src/explorer/radio.ts b/frontend/src/explorer/radio.ts new file mode 100644 index 00000000..b172d8dc --- /dev/null +++ b/frontend/src/explorer/radio.ts @@ -0,0 +1,3 @@ +import { channel } from 'backbone.radio'; + +export default channel('readit-explorer'); diff --git a/frontend/src/explorer/route-actions.ts b/frontend/src/explorer/route-actions.ts new file mode 100644 index 00000000..c0df199f --- /dev/null +++ b/frontend/src/explorer/route-actions.ts @@ -0,0 +1,53 @@ +import View from '../core/view'; +import { source, item as itemNs } from '../jsonld/ns'; +import { Namespace } from '../jsonld/vocabulary'; +import ldChannel from '../jsonld/radio'; +import Node from '../jsonld/node'; +import Controller from './explorer-event-controller'; + +function obtainer(namespace: Namespace) { + return function(serial: string) { + return ldChannel.request('obtain', namespace(serial)); + } +} + +export const getSource = obtainer(source); +export const getItem = obtainer(itemNs); + +export function sourceWithoutAnnotations(control: Controller, node: Node) { + return control.resetSource(node, false); +} + +export function sourceWithAnnotations(control: Controller, node: Node) { + return control.resetSourcePair(node); +} + +export function item(control: Controller, node: Node) { + return control.resetItem(node); +} + +export function itemInEditMode(control: Controller, node: Node) { + return control.editAnnotation(item(control, node), node); +} + +export function itemWithRelations(control: Controller, node: Node) { + return control.listRelated(item(control, node), node); +} + +export function itemWithEditRelations(control: Controller, node: Node) { + return control.editRelated(itemWithRelations(control, node), node); +} + +export function itemWithExternal(control: Controller, node: Node) { + return control.listExternal(item(control, node), node); +} + +export function itemWithEditExternal(control: Controller, node: Node) { + return control.editExternal(itemWithExternal(control, node)); +} + +export function itemWithOccurrences(control: Controller, node: Node) { + // listItemAnnotations does not return the created panel + // see #342 + control.listItemAnnotations(item(control, node), node); +} diff --git a/frontend/src/explorer/route-patterns.ts b/frontend/src/explorer/route-patterns.ts new file mode 100644 index 00000000..e3b318be --- /dev/null +++ b/frontend/src/explorer/route-patterns.ts @@ -0,0 +1,11 @@ +export default { + 'source:bare': 'explore/source/:serial', + 'source:annotated': 'explore/source/:serial/annotations', + 'item': 'explore/item/:serial', + 'item:edit': 'explore/item/:serial/edit', + 'item:related': 'explore/item/:serial/related', + 'item:related:edit': 'explore/item/:serial/related/edit', + 'item:external': 'explore/item/:serial/external', + 'item:external:edit': 'explore/item/:serial/external/edit', + 'item:annotations': 'explore/item/:serial/annotations', +}; diff --git a/frontend/src/explorer/utilities-test.ts b/frontend/src/explorer/utilities-test.ts new file mode 100644 index 00000000..8920c744 --- /dev/null +++ b/frontend/src/explorer/utilities-test.ts @@ -0,0 +1,35 @@ +import explorerChannel from './radio'; +import { announceRoute } from './utilities'; + +describe('explorer utilities', function() { + describe('announceRoute', function() { + beforeEach(function() { + this.spy = jasmine.createSpy('currentRouteSpy'); + this.mockPanel = { + model: { + id: 'http://readit.example/item/20', + }, + }; + explorerChannel.on('currentRoute', this.spy); + }); + + afterEach(function() { + explorerChannel.off(); + }); + + function expectRoute(handler, route) { + handler.call(this.mockPanel); + expect(this.spy).toHaveBeenCalledWith(route, this.mockPanel); + } + + it('creates a function that triggers an event', function() { + const handler = announceRoute('item', ['model', 'id']); + expectRoute.call(this, handler, 'explore/item/20'); + }); + + it('permits plain routes', function() { + const handler = announceRoute('explore'); + expectRoute.call(this, handler, 'explore'); + }); + }); +}); diff --git a/frontend/src/explorer/utilities.ts b/frontend/src/explorer/utilities.ts new file mode 100644 index 00000000..d804ef99 --- /dev/null +++ b/frontend/src/explorer/utilities.ts @@ -0,0 +1,33 @@ +import { get } from 'lodash'; + +import { getLabelFromId } from '../utilities/utilities'; +import explorerChannel from './radio'; +import routePatterns from './route-patterns'; + +/** + * Create an event handler that will report the route of the current + * panel over the `explorerChannel`. + * @param route Name of the route pattern (not the pattern itself) + * @param path Property path relative to `this` to a `Node` from which the + * `':serial'` in the route pattern may be obtained. + * @returns Function that will trigger 'currentRoute' on the `explorerChannel`. + */ +export function announceRoute(route: string, path?: string[]) { + const pattern = routePatterns[route] || route; + + /** + * The created event handler. + * @this Panel view. + * @fires Events#currentRoute + */ + return function(): void { + const serial = getLabelFromId(get(this, path, '')); + const route = pattern.replace(':serial', serial); + + /** + * @event Event#currentRoute + * @type {string, View} + */ + explorerChannel.trigger('currentRoute', route, this); + } +} diff --git a/frontend/src/global/category-styles.ts b/frontend/src/global/category-styles.ts new file mode 100644 index 00000000..46db0937 --- /dev/null +++ b/frontend/src/global/category-styles.ts @@ -0,0 +1,4 @@ +import CategoryStyling from '../utilities/category-colors/category-colors-view'; +import ontology from './ontology'; + +export default new CategoryStyling({ collection: ontology }); diff --git a/frontend/src/global/direction-fsm.ts b/frontend/src/global/direction-fsm.ts deleted file mode 100644 index a573d6d9..00000000 --- a/frontend/src/global/direction-fsm.ts +++ /dev/null @@ -1,3 +0,0 @@ -import DirectionFsm from '../navigation/direction-fsm'; - -export default new DirectionFsm(); diff --git a/frontend/src/global/direction-router.ts b/frontend/src/global/direction-router.ts deleted file mode 100644 index 60b8ad74..00000000 --- a/frontend/src/global/direction-router.ts +++ /dev/null @@ -1,3 +0,0 @@ -import DirectionRouter from '../navigation/direction-router'; - -export default new DirectionRouter(); diff --git a/frontend/src/global/exploration-router.ts b/frontend/src/global/exploration-router.ts new file mode 100644 index 00000000..d57bf306 --- /dev/null +++ b/frontend/src/global/exploration-router.ts @@ -0,0 +1,3 @@ +import ExplorationRouter from '../navigation/exploration-router'; + +export default new ExplorationRouter(); diff --git a/frontend/src/global/explorer-controller.ts b/frontend/src/global/explorer-controller.ts new file mode 100644 index 00000000..52dc1612 --- /dev/null +++ b/frontend/src/global/explorer-controller.ts @@ -0,0 +1,4 @@ +import ExplorerController from '../explorer/explorer-event-controller'; +import explorerView from './explorer-view'; + +export default new ExplorerController(explorerView); diff --git a/frontend/src/global/explorer-view.ts b/frontend/src/global/explorer-view.ts new file mode 100644 index 00000000..dc78f2e8 --- /dev/null +++ b/frontend/src/global/explorer-view.ts @@ -0,0 +1,4 @@ +import View from '../core/view'; +import ExplorerView from '../explorer/explorer-view'; + +export default new ExplorerView({ first: new View() }); diff --git a/frontend/src/global/main-router.ts b/frontend/src/global/main-router.ts new file mode 100644 index 00000000..88698533 --- /dev/null +++ b/frontend/src/global/main-router.ts @@ -0,0 +1,3 @@ +import MainRouter from '../navigation/main-router'; + +export default new MainRouter(); diff --git a/frontend/src/global/ontology.ts b/frontend/src/global/ontology.ts index 9e88f00f..598de7bc 100644 --- a/frontend/src/global/ontology.ts +++ b/frontend/src/global/ontology.ts @@ -1,7 +1,8 @@ /** - * This module provides global access to the ontology. It provides - * this access indirectly. Nothing can be imported from this module; - * access is only possible through the linked data radio channel. The + * This module provides global access to the ontology. While the + * ontology is provided as a default export, this is only meant for + * other global and aspect modules; unit modules should only access + * the ontology indirectly through the linked data radio channel. The * ontology is fetched lazily, i.e., not before it is first requested. * * This module provides its service through one trigger and two @@ -34,6 +35,7 @@ import { readit } from '../jsonld/ns'; import Graph from '../jsonld/graph'; const ontology = new Graph(); +export default ontology; let promise: PromiseLike = null; /** diff --git a/frontend/src/global/readit-ontology.ts b/frontend/src/global/readit-ontology.ts deleted file mode 100644 index bc297d83..00000000 --- a/frontend/src/global/readit-ontology.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Graph from "./../jsonld/graph"; -import mockOntology from "../mock-data/mock-ontology"; - -export default new Graph(mockOntology); diff --git a/frontend/src/global/source-list-view.ts b/frontend/src/global/source-list-view.ts new file mode 100644 index 00000000..fdaf24e8 --- /dev/null +++ b/frontend/src/global/source-list-view.ts @@ -0,0 +1,4 @@ +import SourceListView from '../panel-source-list/source-list-view'; +import sources from '../global/sources'; + +export default new SourceListView({ collection: sources }); diff --git a/frontend/src/global/sources.ts b/frontend/src/global/sources.ts new file mode 100644 index 00000000..8afe67ef --- /dev/null +++ b/frontend/src/global/sources.ts @@ -0,0 +1,13 @@ +import { once } from 'lodash'; + +import { source } from '../jsonld/ns'; +import Graph from '../jsonld/graph'; + +const sources = new Graph(); +export default sources; + +function getSources(): void { + sources.fetch({ url: source() }); +} + +export const ensureSources = once(getSources); diff --git a/frontend/src/layout/menu-template.hbs b/frontend/src/layout/menu-template.hbs index e59dad05..b6c266e1 100644 --- a/frontend/src/layout/menu-template.hbs +++ b/frontend/src/layout/menu-template.hbs @@ -13,8 +13,11 @@