UNPKG

ingenta-lens

Version:
404 lines (330 loc) 13.2 kB
"use strict"; var _ = require("underscore"); var View = require("../substance/application").View; var Data = require("../substance/data"); var Index = Data.Graph.Index; var $$ = require("../substance/application").$$; // Lens.Reader.View // ========================================================================== // var ReaderView = function(readerCtrl) { View.call(this); // Controllers // -------- this.readerCtrl = readerCtrl; this.doc = this.readerCtrl.getDocument(); this.$el.addClass('article'); this.$el.addClass(this.doc.schema.id); // Substance article or lens article? // Stores latest body scroll positions per panel this.bodyScroll = {}; // Panels // ------ // Note: ATM, it is not possible to override the content panel + toc via panelSpecification this.contentView = readerCtrl.panelCtrls.content.createView(); this.tocView = this.contentView.getTocView(); this.panelViews = {}; // mapping to associate reference types to panels // NB, in Lens each resource type has one dedicated panel; // clicking on a reference opens this panel this.panelForRef = {}; _.each(readerCtrl.panels, function(panel) { var name = panel.getName(); var panelCtrl = readerCtrl.panelCtrls[name]; this.panelViews[name] = panelCtrl.createView(); _.each(panel.config.references, function(refType) { this.panelForRef[refType] = name; }, this); }, this); this.panelViews['toc'] = this.tocView; // Keep an index for resources this.resources = new Index(this.readerCtrl.getDocument(), { types: ["resource_reference"], property: "target" }); // whenever a workflow takes control set this variable // to be able to call it a last time when switching to another // workflow this.lastWorkflow = null; this.lastPanel = "toc"; // Events // -------- // this._onTogglePanel = _.bind( this.switchPanel, this ); // Whenever a state change happens (e.g. user navigates somewhere) // the interface gets updated accordingly this.listenTo(this.readerCtrl, "state-changed", this.updateState); this.listenTo(this.tocView,'toggle', this._onTogglePanel); _.each(this.panelViews, function(panelView) { this.listenTo(panelView, "toggle", this._onTogglePanel); this.listenTo(panelView, "toggle-resource", this.onToggleResource); this.listenTo(panelView, "toggle-resource-reference", this.onToggleResourceReference); this.listenTo(panelView, "toggle-fullscreen", this.onToggleFullscreen); }, this); // TODO: treat content panel as panelView and delegate to tocView where necessary this.listenTo(this.contentView, "toggle", this._onTogglePanel); this.listenTo(this.contentView, "toggle-resource", this.onToggleResource); this.listenTo(this.contentView, "toggle-resource-reference", this.onToggleResourceReference); this.listenTo(this.contentView, "toggle-fullscreen", this.onToggleFullscreen); // attach workflows _.each(this.readerCtrl.workflows, function(workflow) { workflow.attach(this.readerCtrl, this); }, this); // attach a lazy/debounced handler for resize events // that updates the outline of the currently active panels $(window).resize(_.debounce(_.bind(function() { this.contentView.scrollbar.update(); var currentPanel = this.panelViews[this.readerCtrl.state.panel]; if (currentPanel && currentPanel.hasScrollbar()) { currentPanel.scrollbar.update(); } }, this), 1)); }; ReaderView.Prototype = function() { // Rendering // -------- // this.render = function() { var frag = document.createDocumentFragment(); // Prepare doc view // -------- frag.appendChild(this.contentView.render().el); // Scrollbar cover // This is only there to cover the content panel's scrollbar in Firefox. var scrollbarCover = $$('.scrollbar-cover'); this.contentView.el.appendChild(scrollbarCover); // Prepare panel toggles // -------- var panelToggles = $$('.context-toggles'); panelToggles.appendChild(this.tocView.getToggleControl()); this.tocView.on('toggle', this._onClickPanel); _.each(this.readerCtrl.panels, function(panel) { var panelView = this.panelViews[panel.getName()]; var toggleEl = panelView.getToggleControl(); panelToggles.appendChild(toggleEl); panelView.on('toggle', this._onClickPanel); }, this); // Prepare panel views // ------- // Wrap everything within resources view var resourcesViewEl = $$('.resources'); resourcesViewEl.appendChild(this.tocView.render().el); _.each(this.readerCtrl.panels, function(panel) { var panelView = this.panelViews[panel.getName()]; // console.log('Rendering panel "%s"', name); resourcesViewEl.appendChild(panelView.render().el); }, this); var menuBar = $$('.menu-bar'); menuBar.appendChild(panelToggles); resourcesViewEl.appendChild(menuBar); frag.appendChild(resourcesViewEl); this.el.appendChild(frag); // TODO: also update the outline after image (et al.) are loaded // Postpone things that expect this view has been inserted into the DOM already. _.delay(_.bind( function() { // initial state update here as scrollTo would not work out of DOM this.updateState(); var self = this; // MathJax requires the processed elements to be in the DOM if (window.MathJax){ window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub]); window.MathJax.Hub.Queue(function () { // HACK: using updateState() instead of updateScrollbars() as it also knows how to scroll self.updateState(); }); } }, this), 1); return this; }; // Free the memory. // -------- // this.dispose = function() { _.each(this.workflows, function(workflow) { workflow.detach(); }); this.contentView.dispose(); _.each(this.panelViews, function(panelView) { panelView.off('toggle', this._onClickPanel); panelView.dispose(); }, this); this.resources.dispose(); this.stopListening(); }; this.getState = function() { return this.readerCtrl.state; }; // Explicit panel switch // -------- // // Only triggered by the explicit switch // Implicit panel switches happen when someone clicks a figure reference this.switchPanel = function(panel) { this.readerCtrl.switchPanel(panel); // keep this so that it gets opened when leaving another panel (toggling reference) this.lastPanel = panel; }; // Update Reader State // -------- // // Called every time the controller state has been modified // Search for readerCtrl.modifyState occurences this.updateState = function() { var self = this; var state = this.readerCtrl.state; var handled; // EXPERIMENTAL: introducing workflows to handle state updates // we extract some info to make it easier for workflows to detect if they // need to handle the state update. var stateInfo = { focussedNode: state.focussedNode ? this.doc.get(state.focussedNode) : null }; var currentPanelView = state.panel === "content" ? this.contentView : this.panelViews[state.panel]; _.each(this.panelViews, function(panelView) { if (!panelView.isHidden()) panelView.hide(); }); // Always deactivate previous highlights this.contentView.removeHighlights(); // and also remove highlights from resource panels _.each(this.panelViews, function(panelView) { panelView.removeHighlights(); }); // Highlight the focussed node if (state.focussedNode) { var classes = ["focussed", "highlighted"]; // HACK: abusing addHighlight for adding the fullscreen class // instead I would prefer to handle such focussing explicitely in a workflow if (state.fullscreen) classes.push("fullscreen"); this.contentView.addHighlight(state.focussedNode, classes.concat('main-occurrence').join(' ')); currentPanelView.addHighlight(state.focussedNode, classes.join(' ')); currentPanelView.scrollTo(state.focussedNode); } // A workflow needs to take care of // 1. showing the correct panel // 2. setting highlights in the content panel // 3. setting highlights in the resource panel // 4. scroll panels // A workflow should have Workflow.handlesStateUpdates = true if it is interested in state updates // and should override Workflow.handleStateUpdate(state, info) to perform the update. // In case it has been responsible for the update it should return 'true'. // TODO: what is this exactly for? if (this.lastWorkflow) { handled = this.lastWorkflow.handleStateUpdate(state, stateInfo); } if (!handled) { // Go through all workflows and let them try to handle the state update. // Stop after the first hit. for (var i = 0; i < this.readerCtrl.workflows.length; i++) { var workflow = this.readerCtrl.workflows[i]; // lastWorkflow had its chance already, so skip it here if (workflow !== this.lastWorkflow && workflow.handlesStateUpdate) { handled = workflow.handleStateUpdate(state, stateInfo); if (handled) { this.lastWorkflow = workflow; break; } } } } // If not handled above, we at least show the correct panel if (!handled) { // Default implementation for states with a panel set if (state.panel !== "content") { var panelView = this.panelViews[state.panel]; this.showPanel(state.panel); // if there is a resource focussed in the panel, activate the resource, and highlight all references to it in the content panel if (state.focussedNode) { // get all references that point to the focussedNode and highlight them var refs = this.resources.get(state.focussedNode); _.each(refs, function(ref) { this.contentView.addHighlight(ref.id, "highlighted "); }, this); // TODO: Jumps to wrong position esp. for figures, because content like images has not completed loading // at that stage. WE should make corrections afterwards if (panelView.hasScrollbar()) panelView.scrollTo(state.focussedNode); } } else { this.showPanel("toc"); } } // HACK: Update the scrollbar after short delay // This was necessary after we went back to using display: none for hiding panels, // instead of visibility: hidden (caused problems with scrolling on iPad) // This hack should not be necessary if we can ensure that // - panel is shown first (so scrollbar can grab the dimensions) // - whenever the contentHeight changes scrollbars should be updated // - e.g. when an image completed loading self.updateScrollbars(); _.delay(function() { self.updateScrollbars(); }, 2000); }; this.updateScrollbars = function() { var state = this.readerCtrl.state; // var currentPanelView = state.panel === "content" ? this.contentView : this.panelViews[state.panel]; this.contentView.scrollbar.update(); _.each(this.panelViews, function(panelView) { if (panelView.hasScrollbar()) panelView.scrollbar.update(); }); // if (currentPanelView && currentPanelView.hasScrollbar()) currentPanelView.scrollbar.update(); }; this.showPanel = function(name) { if (this.panelViews[name]) { this.panelViews[name].activate(); this.el.dataset.context = name; } else if (name === "content") { this.panelViews.toc.activate(); this.el.dataset.context = name; } }; this.getPanelView = function(name) { return this.panelViews[name]; }; // Toggle (off) a resource // -------- // this.onToggleResource = function(panel, id, element) { if (element.classList.contains('highlighted')) { this.readerCtrl.modifyState({ panel: panel, focussedNode: null, fullscreen: false }); } else { this.readerCtrl.modifyState({ panel: panel, focussedNode: id }); } }; // Toggle (off) a reference // -------- this.onToggleResourceReference = function(panel, id, element) { if (element.classList.contains('highlighted')) { this.readerCtrl.modifyState({ panel: this.lastPanel, focussedNode: null, fullscreen: false }); } else { // FIXME: ATM the state always assumes 'content' as the containing panel // Instead, we also let the panel catch the event and then delegate to ReaderView providing the context as done with onToggleResource this.readerCtrl.modifyState({ panel: "content", focussedNode: id, fullscreen: false }); } }; this.onToggleFullscreen = function(panel, id) { var fullscreen = !this.readerCtrl.state.fullscreen; this.readerCtrl.modifyState({ panel: panel, focussedNode: id, fullscreen: fullscreen }); }; }; ReaderView.Prototype.prototype = View.prototype; ReaderView.prototype = new ReaderView.Prototype(); ReaderView.prototype.constructor = ReaderView; module.exports = ReaderView;