<commit_before>import {
  platform, DefaultDOMElement, AbstractEditor,
  Toolbar, EditorSession, DOMSelection
} from 'substance'
import SheetContextSection from './SheetContextSection'
import SheetStatusBar from './SheetStatusBar'
import FormulaBar from './FormulaBar'
import FormulaEditor from './FormulaEditor'

export default class SheetEditor extends AbstractEditor {

  constructor(...args) {
    super(...args)
    // a context for FormulaBar and FormulaEditor
    this._formulaEditorContext = this._createFormulaEditorContext()
    // true when the cursor is either in the FormularBar or the FormulaEditor
    this._isEditing = false

    this.handleActions({
      'updateCell': this._updateCell,
      'cancelCellEditing': this._cancelCellEditing,
      'editCell': this._editCell,
      'requestSelectionChange': this._requestSelectionChange
    })
  }

  getChildContext() {
    let editorSession = this.props.editorSession
    return Object.assign({}, super.getChildContext(), {
      configurator: editorSession.getConfigurator(),
      host: this.props.host,
      issueManager: editorSession.getManager('issue-manager'),
      // TODO: get rid of this
      cellEditorSession: this._cellEditorSession
    })
  }

  getInitialState() {
    return {
      showContext: false,
      contextId: null,
      cellId: null
    }
  }

  didMount() {
    super.didMount()

    const editorSession = this.props.editorSession
    if (platform.inBrowser) {
      DefaultDOMElement.wrap(window).on('resize', this._onResize, this)
    }
    editorSession.onUpdate('selection', this._onSelectionChange, this)

    this._formulaEditorContext.editorSession.onUpdate('selection', this._onCellEditorSelectionChange, this)
    // TODO: is it really necessary to have this in the first line?
    // always render a second time to render for the real element dimensions
    // because the table with fixed layout needs exact dimensions
    this.rerender()
  }

  dispose() {
    super.dispose()
    const editorSession = this.props.editorSession
    if (platform.inBrowser) {
      DefaultDOMElement.wrap(window).off(this)
    }
    editorSession.off(this)
    this._formulaEditorContext.editorSession.off(this)
  }

  render($$) {
    let el = $$('div').addClass('sc-sheet-editor')
    el.append(
      $$('div').addClass('se-main-section').append(
        this._renderToolpane($$),
        this._renderContent($$),
        this._renderStatusbar($$)
      )
    )
    if(this.state.showContext) {
      el.append(
        $$(SheetContextSection, {
          contextId: this.state.contextId,
          cellId: this.state.cellId
        }).ref('context')
      )
    }
    return el
  }

  _renderToolpane($$) {
    const configurator = this.getConfigurator()
    let el = $$('div').addClass('se-tool-pane')
    el.append(
      $$(FormulaBar, {
        node: this._formulaEditorContext.node,
        context: this._formulaEditorContext
      }),
      $$(Toolbar, {
        toolPanel: configurator.getToolPanel('toolbar')
      }).ref('toolbar')
    )
    return el
  }

  _renderContent($$) {
    let el = $$('div').addClass('se-body')
    el.append(
      this._renderSheet($$)
    )
    return el
  }

  _renderSheet($$) {
    const sheet = this.getDocument()
    const formulaEditorContext = this._formulaEditorContext
    // only rendering the sheet when mounted
    // so that we have real width and height
    if (this.isMounted()) {
      const SheetComponent = this.getComponent('sheet')
      return $$(SheetComponent, {
        sheet,
        overlays: [
          $$(FormulaEditor, {
            context: formulaEditorContext
          })
            .ref('formulaEditor')
            .css({
              position: 'absolute',
              display: 'none'
            })
        ]
      }).ref('sheet')
    } else {
      return $$('div')
    }
  }

  _renderStatusbar($$) {
    return $$(SheetStatusBar, {}).ref('sheet-statusbar')
  }

  getWidth() {
    if (this.el) {
      return this.el.getWidth()
    } else {
      return 1000
    }
  }

  getHeight() {
    if (this.el) {
      return this.el.getHeight()
    } else {
      return 750
    }
  }

  setSelectionOnCell(cellId) {
    const sheet = this.getDocument()
    let cell = sheet.get(cellId)
    let row = cell.parentNode
    let colIdx = row._childNodes.indexOf(cell.id)
    let rowIdx = row.parentNode._childNodes.indexOf(row.id)
    let selData = {
      type: 'range',
      anchorRow: rowIdx,
      focusRow: rowIdx,
      anchorCol: colIdx,
      focusCol: colIdx
    }

    this.props.editorSession.setSelection({
      type: 'custom',
      customType: 'sheet',
      data: selData,
      surfaceId: this.refs.sheet.getId()
    })
  }

  setSelectionOnSheet() {
    const sheet = this.getDocument()
    let selData = {
      type: 'range',
      anchorRow: 0,
      focusRow: sheet.getRowCount() - 1,
      anchorCol: 0,
      focusCol: sheet.getColumnCount() - 1
    }

    this.props.editorSession.setSelection({
      type: 'custom',
      customType: 'sheet',
      data: selData,
      surfaceId: this.refs.sheet.getId()
    })
  }

  toggleContext(contextId, cellId) {
    if(cellId === null && !this.state.showContext) return
    if (this.state.showContext && this.state.contextId === contextId && cellId === undefined) {
      this.setState({
        showContext: false
      })
    } else {
      this.setState({
        showContext: true,
        contextId,
        cellId
      })
    }
  }

  // a context propagated by FormulaBar and FormulaEditor
  _createFormulaEditorContext() {
    const configurator = this.props.editorSession.configurator
    // a document with only one node used by cell editors
    // i.e. expression bar, or popover editor on enter
    let cellEditorDoc = this.getDocument().newInstance()
    // TODO: use an id instead of storing the node on the doc
    let node = cellEditorDoc.createElement('cell')
    let editorSession = new EditorSession(cellEditorDoc, {
      configurator: configurator
    })
    const self = this
    // HACK: creating inline an object which DOMSelection needs to work
    // TODO: either use a helper to create the DOMSelection
    // or change DOMSelection's ctor to be better usable
    let domSelection = new DOMSelection({
      getDocument() { return cellEditorDoc },
      getSurfaceManager() { return editorSession.surfaceManager },
      getElement() { return self.getElement() }
    })
    return {
      editorSession,
      domSelection,
      node,
      markersManager: editorSession.markersManager,
      surfaceManager: editorSession.surfaceManager
    }
  }

  _onResize() {
    if (platform.inBrowser) {
      if (!this._rafId) {
        this._rafId = window.requestAnimationFrame(() => {
          this._rafId = null
          this.refs.sheet.resize()
        })
      }
    }
  }

  _getSheetSelection() {
    return this.getEditorSession().getSelection().data || {}
  }

  _onSelectionChange() {
    let formulaEditorSession = this._formulaEditorContext.editorSession
    let cell = this._getAnchorCell()
    this._setFormula(cell.textContent)
    if (this._isEditing) {
      this._isEditing = false
      this._hideFormulaEditor()
      formulaEditorSession.setSelection(null)
    }
  }

  _onCellEditorSelectionChange(sel) {
    let sheetSel = this._getSheetSelection()
    let formulaEditorSession = this._formulaEditorContext.editorSession
    if (!sel.isNull() && !this._isEditing) {
      this._isEditing = true
      this._currentSelection = this.getEditorSession().getSelection()
      this._showFormulaEditor(sheetSel.anchorRow, sheetSel.anchorCol)
      formulaEditorSession.resetHistory()
    }
  }

  /*
    This gets called when the user starts editing a cell
    At this time it should be sure that the table cell
    is already rendered.
  */
  _showFormulaEditor(rowIdx, colIdx) {
    const formulaEditor = this.refs.formulaEditor
    let rect = this.refs.sheet.getCellRect(rowIdx, colIdx)
    formulaEditor.css({
      display: 'block',
      position: 'absolute',
      top: rect.top,
      left: rect.left,
      "min-width": rect.width+'px',
      "min-height": rect.height+'px'
    })
  }

  _hideFormulaEditor() {
    const formulaEditor = this.refs.formulaEditor
    formulaEditor.css({
      display: 'none',
      top: 0, left: 0
    })
  }

  _setFormula(val, sel) {
    let context = this._formulaEditorContext
    let node = context.node
    let formulaEditorSession = this._formulaEditorContext.editorSession
    formulaEditorSession.transaction(tx => {
      tx.set(node.getPath(), val)
      tx.setSelection(sel)
    })
  }

  _cancelCellEditing() {
    // just renew the the selection
    let editorSession = this.getEditorSession()
    editorSession.setSelection(editorSession.getSelection())
  }

  /*
    Request inline editor
  */
  _editCell(initialValue) {
    let formulaEditorSession = this._formulaEditorContext.editorSession
    let formulaNode = this._formulaEditorContext.node
    if (initialValue) {
      this._setFormula(initialValue)
    }
    let pos = formulaNode.textContent.length
    formulaEditorSession.setSelection({
      type: 'property',
      path: formulaNode.getPath(),
      startOffset: pos,
      surfaceId: 'formula-editor'
    })
  }

  _requestSelectionChange(newSelection) {
    let editorSession = this.getEditorSession()
    if (this._isEditing) {
      console.info('TODO: Implement range selector', newSelection)
    } else {
      editorSession.setSelection(newSelection)
    }
  }

  _updateCell() {
    let editorSession = this.getEditorSession()
    let cell = this._getAnchorCell()
    let newValue = this._formulaEditorContext.node.getText()
    // HACK: need to set the selection 'silently' so that
    // this works fine with undo/redo (-> before state)
    // plus: setting the selection in the transaction
    // leads to an inintuitiv undo/redo behavior
    // thus we are updating the selection in an extra update (which is not landing in the history)
    let newSel = this.refs.sheet.shiftSelection(1, 0, false)
    editorSession._setSelection(this._currentSelection)
    editorSession.transaction(tx => {
      tx.set(cell.getPath(), newValue)
    })
    editorSession.setSelection(newSel)
  }

  _getAnchorCell() {
    let sel = this._getSheetSelection()
    return this.getDocument().getCell(sel.anchorRow, sel.anchorCol)
  }

}
<commit_msg>Fix SheetEditor<commit_after>import {
  platform, DefaultDOMElement, AbstractEditor,
  Toolbar, EditorSession, DOMSelection
} from 'substance'
import SheetContextSection from './SheetContextSection'
import SheetStatusBar from './SheetStatusBar'
import FormulaBar from './FormulaBar'
import FormulaEditor from './FormulaEditor'

export default class SheetEditor extends AbstractEditor {

  constructor(...args) {
    super(...args)
    // a context for FormulaBar and FormulaEditor
    this._formulaEditorContext = this._createFormulaEditorContext()
    // true when the cursor is either in the FormularBar or the FormulaEditor
    this._isEditing = false

    this.handleActions({
      'updateCell': this._updateCell,
      'cancelCellEditing': this._cancelCellEditing,
      'editCell': this._editCell,
      'requestSelectionChange': this._requestSelectionChange
    })
  }

  getChildContext() {
    let editorSession = this.props.editorSession
    return Object.assign({}, super.getChildContext(), {
      configurator: editorSession.getConfigurator(),
      host: this.props.host,
      issueManager: editorSession.getManager('issue-manager'),
      // TODO: get rid of this
      cellEditorSession: this._cellEditorSession
    })
  }

  getInitialState() {
    return {
      showContext: false,
      contextId: null,
      cellId: null
    }
  }

  didMount() {
    super.didMount()

    const editorSession = this.props.editorSession
    if (platform.inBrowser) {
      DefaultDOMElement.wrap(window).on('resize', this._onResize, this)
    }
    editorSession.onUpdate('selection', this._onSelectionChange, this)

    this._formulaEditorContext.editorSession.onUpdate('selection', this._onCellEditorSelectionChange, this)
    // TODO: is it really necessary to have this in the first line?
    // always render a second time to render for the real element dimensions
    // because the table with fixed layout needs exact dimensions
    this.rerender()
  }

  dispose() {
    super.dispose()
    const editorSession = this.props.editorSession
    if (platform.inBrowser) {
      DefaultDOMElement.wrap(window).off(this)
    }
    editorSession.off(this)
    this._formulaEditorContext.editorSession.off(this)
  }

  render($$) {
    let el = $$('div').addClass('sc-sheet-editor')
    el.append(
      $$('div').addClass('se-main-section').append(
        this._renderToolpane($$),
        this._renderContent($$),
        this._renderStatusbar($$)
      )
    )
    if(this.state.showContext) {
      el.append(
        $$(SheetContextSection, {
          contextId: this.state.contextId,
          cellId: this.state.cellId
        }).ref('context')
      )
    }
    return el
  }

  _renderToolpane($$) {
    const configurator = this.getConfigurator()
    let el = $$('div').addClass('se-tool-pane')
    el.append(
      $$(FormulaBar, {
        node: this._formulaEditorContext.node,
        context: this._formulaEditorContext
      }),
      $$(Toolbar, {
        toolPanel: configurator.getToolPanel('toolbar')
      }).ref('toolbar')
    )
    return el
  }

  _renderContent($$) {
    let el = $$('div').addClass('se-body')
    el.append(
      this._renderSheet($$)
    )
    return el
  }

  _renderSheet($$) {
    const sheet = this.getDocument()
    const formulaEditorContext = this._formulaEditorContext
    // only rendering the sheet when mounted
    // so that we have real width and height
    if (this.isMounted()) {
      const SheetComponent = this.getComponent('sheet')
      return $$(SheetComponent, {
        sheet,
        overlays: [
          $$(FormulaEditor, {
            context: formulaEditorContext
          })
            .ref('formulaEditor')
            .css({
              position: 'absolute',
              display: 'none'
            })
        ]
      }).ref('sheet')
    } else {
      return $$('div')
    }
  }

  _renderStatusbar($$) {
    return $$(SheetStatusBar, {}).ref('sheet-statusbar')
  }

  getWidth() {
    if (this.el) {
      return this.el.getWidth()
    } else {
      return 1000
    }
  }

  getHeight() {
    if (this.el) {
      return this.el.getHeight()
    } else {
      return 750
    }
  }

  setSelectionOnCell(cellId) {
    const sheet = this.getDocument()
    let cell = sheet.get(cellId)
    let row = cell.parentNode
    let colIdx = row._childNodes.indexOf(cell.id)
    let rowIdx = row.parentNode._childNodes.indexOf(row.id)
    let selData = {
      type: 'range',
      anchorRow: rowIdx,
      focusRow: rowIdx,
      anchorCol: colIdx,
      focusCol: colIdx
    }

    this.props.editorSession.setSelection({
      type: 'custom',
      customType: 'sheet',
      data: selData,
      surfaceId: this.refs.sheet.getId()
    })
  }

  setSelectionOnSheet() {
    const sheet = this.getDocument()
    let selData = {
      type: 'range',
      anchorRow: 0,
      focusRow: sheet.getRowCount() - 1,
      anchorCol: 0,
      focusCol: sheet.getColumnCount() - 1
    }

    this.props.editorSession.setSelection({
      type: 'custom',
      customType: 'sheet',
      data: selData,
      surfaceId: this.refs.sheet.getId()
    })
  }

  toggleContext(contextId, cellId) {
    if(cellId === null && !this.state.showContext) return
    if (this.state.showContext && this.state.contextId === contextId && cellId === undefined) {
      this.setState({
        showContext: false
      })
    } else {
      this.setState({
        showContext: true,
        contextId,
        cellId
      })
    }
  }

  // a context propagated by FormulaBar and FormulaEditor
  _createFormulaEditorContext() {
    const configurator = this.props.editorSession.configurator
    // a document with only one node used by cell editors
    // i.e. expression bar, or popover editor on enter
    let cellEditorDoc = this.getDocument().newInstance()
    // TODO: use an id instead of storing the node on the doc
    let node = cellEditorDoc.createElement('cell')
    let editorSession = new EditorSession(cellEditorDoc, {
      configurator: configurator
    })
    const self = this
    // HACK: creating inline an object which DOMSelection needs to work
    // TODO: either use a helper to create the DOMSelection
    // or change DOMSelection's ctor to be better usable
    let domSelection = new DOMSelection({
      getDocument() { return cellEditorDoc },
      getSurfaceManager() { return editorSession.surfaceManager },
      getElement() { return self.getElement() }
    })
    return {
      editorSession,
      domSelection,
      node,
      markersManager: editorSession.markersManager,
      surfaceManager: editorSession.surfaceManager
    }
  }

  _onResize() {
    if (platform.inBrowser) {
      if (!this._rafId) {
        this._rafId = window.requestAnimationFrame(() => {
          this._rafId = null
          this.refs.sheet.resize()
        })
      }
    }
  }

  _getSheetSelection() {
    return this.getEditorSession().getSelection().data || {}
  }

  _onSelectionChange() {
    let formulaEditorSession = this._formulaEditorContext.editorSession
    let cell = this._getAnchorCell()
    this._setFormula(cell.textContent)
    if (this._isEditing) {
      this._isEditing = false
      this._hideFormulaEditor()
      formulaEditorSession.setSelection(null)
    }
  }

  _onCellEditorSelectionChange(sel) {
    let sheetSel = this._getSheetSelection()
    let formulaEditorSession = this._formulaEditorContext.editorSession
    if (!sel.isNull() && !this._isEditing) {
      this._isEditing = true
      this._currentSelection = this.getEditorSession().getSelection()
      this._showFormulaEditor(sheetSel.anchorRow, sheetSel.anchorCol)
      formulaEditorSession.resetHistory()
    }
  }

  /*
    This gets called when the user starts editing a cell
    At this time it should be sure that the table cell
    is already rendered.
  */
  _showFormulaEditor(rowIdx, colIdx) {
    const formulaEditor = this.refs.formulaEditor
    let rect = this.refs.sheet.getCellRect(rowIdx, colIdx)
    formulaEditor.css({
      display: 'block',
      position: 'absolute',
      top: rect.top,
      left: rect.left,
      "min-width": rect.width+'px',
      "min-height": rect.height+'px'
    })
  }

  _hideFormulaEditor() {
    const formulaEditor = this.refs.formulaEditor
    formulaEditor.css({
      display: 'none',
      top: 0, left: 0
    })
  }

  _setFormula(val, sel) {
    let context = this._formulaEditorContext
    let node = context.node
    let formulaEditorSession = this._formulaEditorContext.editorSession
    formulaEditorSession.transaction(tx => {
      tx.set(node.getPath(), val)
      tx.setSelection(sel)
    })
  }

  _cancelCellEditing() {
    // just renew the the selection
    let editorSession = this.getEditorSession()
    editorSession.setSelection(editorSession.getSelection())
  }

  /*
    Request inline editor
  */
  _editCell(initialValue) {
    let formulaEditorSession = this._formulaEditorContext.editorSession
    let formulaNode = this._formulaEditorContext.node
    if (initialValue) {
      this._setFormula(initialValue)
    }
    let pos = formulaNode.textContent.length
    formulaEditorSession.setSelection({
      type: 'property',
      path: formulaNode.getPath(),
      startOffset: pos,
      surfaceId: 'formula-editor'
    })
  }

  _requestSelectionChange(newSelection) {
    let editorSession = this.getEditorSession()
    if (this._isEditing) {
      console.info('TODO: Implement range selector', newSelection)
    } else {
      editorSession.setSelection(newSelection)
    }
  }

  _updateCell() {
    let editorSession = this.getEditorSession()
    let cell = this._getAnchorCell()
    let newValue = this._formulaEditorContext.node.getText()
    // HACK: need to set the selection 'silently' so that
    // this works fine with undo/redo (-> before state)
    // plus: setting the selection in the transaction
    // leads to an inintuitiv undo/redo behavior
    // thus we are updating the selection in an extra update (which is not landing in the history)
    let newSel = this.refs.sheet.shiftSelection(1, 0, false)
    let selBefore = this._currentSelection
    // Note: collapsing the selection to the anchor cell
    // so that on undo/redo only the change cell is selected
    selBefore.data.focusRow = selBefore.data.anchorRow
    selBefore.data.focusCol = selBefore.data.anchorCol
    editorSession._setSelection(selBefore)
    editorSession.transaction(tx => {
      tx.set(cell.getPath(), newValue)
    })
    editorSession.setSelection(newSel)
  }

  _getAnchorCell() {
    let sel = this._getSheetSelection()
    return this.getDocument().getCell(sel.anchorRow, sel.anchorCol)
  }

}
<|endoftext|>


<commit_before>var RegistryClient = require('../Client'),
    fs = require('fs'),
    expect = require('expect.js'),
    nock = require('nock');

describe('RegistryClient', function () {

    beforeEach(function () {
        this.uri = 'https://bower.herokuapp.com';
        this.timeoutVal = 5000;
        this.registry = new RegistryClient({
            strictSsl: false,
            timeout: this.timeoutVal
        });
        this.conf = {
            search: [this.uri],
            register: this.uri,
            publish: this.uri
        };
    });

    describe('Constructor', function () {

        describe('instantiating a client', function () {
            it('should provide an instance of RegistryClient', function () {
                expect(this.registry instanceof RegistryClient).to.be.ok;
            });

            it('should set default registry config', function () {
                expect(this.registry._config.registry).to.eql(this.conf);
            });

            it('should set default search config', function () {
                expect(this.registry._config.registry.search[0]).to.eql(this.uri);
            });

            it('should set default register config', function () {
                expect(this.registry._config.registry.register).to.eql(this.uri);
            });

            it('should set default publish config', function () {
                expect(this.registry._config.registry.publish).to.eql(this.uri);
            });

            it('should set default cache path config', function () {
                expect(typeof this.registry._config.cache === 'string').to.be.ok;
            });

            it('should set default timeout config', function () {
                expect(this.registry._config.timeout).to.eql(this.timeoutVal);
            });

            it('should set default strictSsl config', function () {
                expect(this.registry._config.strictSsl).to.be(false);
            });
        });

        it('should have a lookup prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('lookup');
        });

        it('should have a search prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('search');
        });

        it('should have a list prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('list');
        });

        it('should have a register prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('register');
        });

        it('should have a clearCache prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('clearCache');
        });

        it('should have a resetCache prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('resetCache');
        });

        it('should have a clearRuntimeCache static method', function () {
            expect(RegistryClient).to.have.property('clearRuntimeCache');
        });
    });

    describe('instantiating a client with custom options', function () {

        describe('offline', function () {

            it('should not return search results ', function (next) {
                this.registry._config.offline = true;
                this.registry.search('jquery', function (err, results) {
                    expect(err).to.be(null);
                    expect(results.length).to.eql(0);
                    next();
                });
            });
        });

        describe('cache', function () {
            beforeEach(function () {
                nock('https://bower.herokuapp.com:443')
                  .get('/packages/search/jquery')
                  .replyWithFile(200, __dirname + '/fixtures/search.json');

                this.client = new RegistryClient({
                    cache: __dirname + '/cache',
                    strictSsl: false
                });

                this.cacheDir = this.client._config.cache;
                this.host = 'bower.herokuapp.com';
                this.method = 'search';
                this.pkg = 'jquery';

                this.path = this.cacheDir + '/' + this.host + '/' + this.method + '/' + this.pkg;
            });

            afterEach(function (next) {
                this.client.clearCache(next);
            });

            it('should fill cache', function (next) {
                var self = this;

                // fill cache
                self.client.search(self.pkg, function (err, results) {
                    expect(err).to.be(null);
                    expect(results.length).to.eql(334);

                    // check for cache existence
                    fs.exists(self.path, function (exists) {
                        expect(exists).to.be(true);
                        next();
                    });
                });

            });

            it('should read results from cache', function (next) {
                var self = this;

                self.client.search(self.pkg, function (err, results) {
                    expect(err).to.be(null);
                    expect(results.length).to.eql(334);

                    fs.exists(self.path, function (exists) {
                        expect(exists).to.be(true);
                        next();
                    });
                });
            });
        });
    });

    //
    // lookup
    //
    describe('calling the lookup instance method with argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/jquery')
              .reply(200, {
                name: 'jquery',
                url: 'git://github.com/components/jquery.git'
            });

            this.registry._config.force = true;
        });

        it('should not return an error', function (next) {
            this.registry.lookup('jquery', function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return entry type', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry.type).to.eql('alias');
                next();
            });
        });

        it('should return entry url ', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry.url).to.eql('git://github.com/components/jquery.git');
            });
            next();
        });
    });

    describe('calling the lookup instance method with three registries', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/jquery')
              .reply(404);

            nock('http://custom-registry.com')
              .get('/packages/jquery')
              .reply(200, {
                name: 'jquery',
                url: 'git://github.com/foo/bar'
            });

            nock('http://custom-registry2.com')
              .get('/packages/jquery')
              .reply(200, {
                name: 'jquery',
                url: 'git://github.com/foo/baz'
            });

            this.registry = new RegistryClient({
                strictSsl: false,
                force: true,
                registry: {
                    search: [
                        'https://bower.herokuapp.com',
                        'http://custom-registry.com',
                        'http://custom-registry2.com'
                    ]
                }
            });
        });

        it('should return entry type', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry).to.be.an('object');
                expect(entry.type).to.eql('alias');
                next();
            });
        });

        it('should return entry url ', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry).to.be.an('object');
                expect(entry.url).to.eql('git://github.com/foo/bar');
                next();
            });
        });

        it('should respect order', function (next) {
            this.registry._config.registry.search = [
                'https://bower.herokuapp.com',
                'http://custom-registry2.com',
                'http://custom-registry.com'
            ];

            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry).to.be.an('object');
                expect(entry.url).to.eql('git://github.com/foo/baz');
                next();
            });
        });
    });

    describe('calling the lookup instance method without argument', function () {
        it('should return an error and no result', function (next) {
            this.registry.lookup('', function (err, entry) {
                expect(err).to.be.an(Error);
                expect(entry).to.be(undefined);
                next();
            });
        });
    });

    //
    // register
    //
    describe('calling the register instance method with argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .post('/packages', 'name=test-ba&url=git%3A%2F%2Fgithub.com%2Ftest-ba%2Ftest-ba.git')
              .reply(201);

            this.pkg = 'test-ba';
            this.pkgUrl = 'git://github.com/test-ba/test-ba.git';
        });

        it('should not return an error', function (next) {
            this.registry.register(this.pkg, this.pkgUrl, function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.register(this.pkg, this.pkgUrl, function (err, entry) {
                expect(err).to.be(null);
                expect(entry.name).to.eql(self.pkg);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.register(this.pkg, this.pkgUrl, function (err, entry) {
                expect(err).to.be(null);
                expect(entry.url).to.eql(self.pkgUrl);
                next();
            });
        });
    });

    describe('calling the register instance method without arguments', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .post('/packages', 'name=&url=')
              .reply(400);
        });

        it('should return an error and no result', function (next) {
            this.registry.register('', '', function (err, entry) {
                expect(err).to.be.an(Error);
                expect(entry).to.be(undefined);
                next();
            });
        });
    });

    //
    // search
    //
    describe('calling the search instance method with argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/search/jquery')
              .replyWithFile(200, __dirname + '/fixtures/search.json');

            this.pkg = 'jquery';
            this.pkgUrl = 'git://github.com/components/jquery.git';

            this.registry._config.force = true;
        });

        it('should not return an error', function (next) {
            this.registry.search(this.pkg, function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                var found = results.some(function (entry) {
                    return entry.name === self.pkg;
                });

                expect(found).to.be(true);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                var found = results.some(function (entry) {
                    return entry.url === self.pkgUrl;
                });

                expect(found).to.be(true);
                next();
            });
        });
    });

    describe('calling the search instance method with two registries', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/search/jquery')
              .reply(200, []);

            nock('http://custom-registry.com')
              .get('/packages/search/jquery')
              .reply(200, [
                {
                    name: 'jquery',
                    url: 'git://github.com/bar/foo.git'
                }
            ]);

            this.pkg = 'jquery';
            this.pkgUrl = 'git://github.com/bar/foo.git';

            this.registry = new RegistryClient({
                strictSsl: false,
                force: true,
                registry: {
                    search: [
                        'https://bower.herokuapp.com',
                        'http://custom-registry.com'
                    ]
                }
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                var found = results.some(function (entry) {
                    return entry.name === self.pkg;
                });

                expect(found).to.be(true);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                if (! results.length) {
                    return next(new Error('Result expected'));
                }

                var found = results.some(function (entry) {
                    return entry.url === self.pkgUrl;
                });

                expect(found).to.be(true);
                next();
            });
        });
    });

    describe('calling the search instance method without argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/search/')
              .reply(404);
        });

        it('should return an error and no results', function (next) {
            this.registry.search('', function (err, results) {
                expect(err).to.be.an(Error);
                expect(results).to.be(undefined);
                next();
            });
        });
    });

    //
    // list
    //
    describe('calling the list instance method', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages')
              .reply(200, [], {});

            this.registry._config.force = true;
        });

        it('should not return an error', function (next) {
            this.registry.list(function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return results array', function (next) {
            var self = this;

            this.registry.list(function (err, results) {
                expect(results).to.be.an('array');
                next();
            });
        });

    });

    describe('calling the list instance method with two registries', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages')
              .reply(200, []);

            nock('http://custom-registry.com')
              .get('/packages')
              .reply(200, [
                {
                    name: 'jquery',
                    url: 'git://github.com/bar/foo.git'
                }
            ]);

            this.registry = new RegistryClient({
                strictSsl: false,
                force: true,
                registry: {
                    search: [
                        'https://bower.herokuapp.com',
                        'http://custom-registry.com'
                    ]
                }
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.list(function (err, results) {
                var found = results.some(function (entry) {
                    return entry.name === self.pkg;
                });

                expect(found).to.be(true);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.list(function (err, results) {
                if (! results.length) {
                    return next(new Error('Result expected'));
                }

                var found = results.some(function (entry) {
                    return entry.url === self.pkgUrl;
                });

                expect(found).to.be(true);
                next();
            });
        });
    });

    describe('calling the list instance method', function () {

        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages')
              .reply(200, [], {});
        });

        it('should return an error and no results', function (next) {
            this.registry.list(function (err) {
                expect(err).to.be(null);
                next();
            });
        });
    });

    //
    // clearCache
    //
    describe('called the clearCache instance method with argument', function () {
        beforeEach(function () {
            this.pkg = 'jquery';
        });

        it('should not return an error', function (next) {
            this.registry.clearCache(this.pkg, function (err) {
                expect(err).to.be(null);
                next();
            });
        });
    });

    describe('called the clearCache instance method without argument', function () {
        it('should not return any errors and remove all cache items', function (next) {
            this.registry.clearCache(function (err) {
                expect(err).to.be(null);
                next();
            });
        });
    });
});
<commit_msg>Remove unused var.<commit_after>var RegistryClient = require('../Client'),
    fs = require('fs'),
    expect = require('expect.js'),
    nock = require('nock');

describe('RegistryClient', function () {
    beforeEach(function () {
        this.uri = 'https://bower.herokuapp.com';
        this.timeoutVal = 5000;
        this.registry = new RegistryClient({
            strictSsl: false,
            timeout: this.timeoutVal
        });
        this.conf = {
            search: [this.uri],
            register: this.uri,
            publish: this.uri
        };
    });

    describe('Constructor', function () {
        describe('instantiating a client', function () {
            it('should provide an instance of RegistryClient', function () {
                expect(this.registry instanceof RegistryClient).to.be.ok;
            });

            it('should set default registry config', function () {
                expect(this.registry._config.registry).to.eql(this.conf);
            });

            it('should set default search config', function () {
                expect(this.registry._config.registry.search[0]).to.eql(this.uri);
            });

            it('should set default register config', function () {
                expect(this.registry._config.registry.register).to.eql(this.uri);
            });

            it('should set default publish config', function () {
                expect(this.registry._config.registry.publish).to.eql(this.uri);
            });

            it('should set default cache path config', function () {
                expect(typeof this.registry._config.cache === 'string').to.be.ok;
            });

            it('should set default timeout config', function () {
                expect(this.registry._config.timeout).to.eql(this.timeoutVal);
            });

            it('should set default strictSsl config', function () {
                expect(this.registry._config.strictSsl).to.be(false);
            });
        });

        it('should have a lookup prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('lookup');
        });

        it('should have a search prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('search');
        });

        it('should have a list prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('list');
        });

        it('should have a register prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('register');
        });

        it('should have a clearCache prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('clearCache');
        });

        it('should have a resetCache prototype method', function () {
            expect(RegistryClient.prototype).to.have.property('resetCache');
        });

        it('should have a clearRuntimeCache static method', function () {
            expect(RegistryClient).to.have.property('clearRuntimeCache');
        });
    });

    describe('instantiating a client with custom options', function () {
        describe('offline', function () {
            it('should not return search results ', function (next) {
                this.registry._config.offline = true;
                this.registry.search('jquery', function (err, results) {
                    expect(err).to.be(null);
                    expect(results.length).to.eql(0);
                    next();
                });
            });
        });

        describe('cache', function () {
            beforeEach(function () {
                nock('https://bower.herokuapp.com:443')
                  .get('/packages/search/jquery')
                  .replyWithFile(200, __dirname + '/fixtures/search.json');

                this.client = new RegistryClient({
                    cache: __dirname + '/cache',
                    strictSsl: false
                });

                this.cacheDir = this.client._config.cache;
                this.host = 'bower.herokuapp.com';
                this.method = 'search';
                this.pkg = 'jquery';

                this.path = this.cacheDir + '/' + this.host + '/' + this.method + '/' + this.pkg;
            });

            afterEach(function (next) {
                this.client.clearCache(next);
            });

            it('should fill cache', function (next) {
                var self = this;

                // fill cache
                self.client.search(self.pkg, function (err, results) {
                    expect(err).to.be(null);
                    expect(results.length).to.eql(334);

                    // check for cache existence
                    fs.exists(self.path, function (exists) {
                        expect(exists).to.be(true);
                        next();
                    });
                });

            });

            it('should read results from cache', function (next) {
                var self = this;

                self.client.search(self.pkg, function (err, results) {
                    expect(err).to.be(null);
                    expect(results.length).to.eql(334);

                    fs.exists(self.path, function (exists) {
                        expect(exists).to.be(true);
                        next();
                    });
                });
            });
        });
    });

    //
    // lookup
    //
    describe('calling the lookup instance method with argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/jquery')
              .reply(200, {
                name: 'jquery',
                url: 'git://github.com/components/jquery.git'
            });

            this.registry._config.force = true;
        });

        it('should not return an error', function (next) {
            this.registry.lookup('jquery', function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return entry type', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry.type).to.eql('alias');
                next();
            });
        });

        it('should return entry url ', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry.url).to.eql('git://github.com/components/jquery.git');
            });
            next();
        });
    });

    describe('calling the lookup instance method with three registries', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/jquery')
              .reply(404);

            nock('http://custom-registry.com')
              .get('/packages/jquery')
              .reply(200, {
                name: 'jquery',
                url: 'git://github.com/foo/bar'
            });

            nock('http://custom-registry2.com')
              .get('/packages/jquery')
              .reply(200, {
                name: 'jquery',
                url: 'git://github.com/foo/baz'
            });

            this.registry = new RegistryClient({
                strictSsl: false,
                force: true,
                registry: {
                    search: [
                        'https://bower.herokuapp.com',
                        'http://custom-registry.com',
                        'http://custom-registry2.com'
                    ]
                }
            });
        });

        it('should return entry type', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry).to.be.an('object');
                expect(entry.type).to.eql('alias');
                next();
            });
        });

        it('should return entry url ', function (next) {
            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry).to.be.an('object');
                expect(entry.url).to.eql('git://github.com/foo/bar');
                next();
            });
        });

        it('should respect order', function (next) {
            this.registry._config.registry.search = [
                'https://bower.herokuapp.com',
                'http://custom-registry2.com',
                'http://custom-registry.com'
            ];

            this.registry.lookup('jquery', function (err, entry) {
                expect(err).to.be(null);
                expect(entry).to.be.an('object');
                expect(entry.url).to.eql('git://github.com/foo/baz');
                next();
            });
        });
    });

    describe('calling the lookup instance method without argument', function () {
        it('should return an error and no result', function (next) {
            this.registry.lookup('', function (err, entry) {
                expect(err).to.be.an(Error);
                expect(entry).to.be(undefined);
                next();
            });
        });
    });

    //
    // register
    //
    describe('calling the register instance method with argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .post('/packages', 'name=test-ba&url=git%3A%2F%2Fgithub.com%2Ftest-ba%2Ftest-ba.git')
              .reply(201);

            this.pkg = 'test-ba';
            this.pkgUrl = 'git://github.com/test-ba/test-ba.git';
        });

        it('should not return an error', function (next) {
            this.registry.register(this.pkg, this.pkgUrl, function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.register(this.pkg, this.pkgUrl, function (err, entry) {
                expect(err).to.be(null);
                expect(entry.name).to.eql(self.pkg);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.register(this.pkg, this.pkgUrl, function (err, entry) {
                expect(err).to.be(null);
                expect(entry.url).to.eql(self.pkgUrl);
                next();
            });
        });
    });

    describe('calling the register instance method without arguments', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .post('/packages', 'name=&url=')
              .reply(400);
        });

        it('should return an error and no result', function (next) {
            this.registry.register('', '', function (err, entry) {
                expect(err).to.be.an(Error);
                expect(entry).to.be(undefined);
                next();
            });
        });
    });

    //
    // search
    //
    describe('calling the search instance method with argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/search/jquery')
              .replyWithFile(200, __dirname + '/fixtures/search.json');

            this.pkg = 'jquery';
            this.pkgUrl = 'git://github.com/components/jquery.git';

            this.registry._config.force = true;
        });

        it('should not return an error', function (next) {
            this.registry.search(this.pkg, function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                var found = results.some(function (entry) {
                    return entry.name === self.pkg;
                });

                expect(found).to.be(true);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                var found = results.some(function (entry) {
                    return entry.url === self.pkgUrl;
                });

                expect(found).to.be(true);
                next();
            });
        });
    });

    describe('calling the search instance method with two registries', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/search/jquery')
              .reply(200, []);

            nock('http://custom-registry.com')
              .get('/packages/search/jquery')
              .reply(200, [
                {
                    name: 'jquery',
                    url: 'git://github.com/bar/foo.git'
                }
            ]);

            this.pkg = 'jquery';
            this.pkgUrl = 'git://github.com/bar/foo.git';

            this.registry = new RegistryClient({
                strictSsl: false,
                force: true,
                registry: {
                    search: [
                        'https://bower.herokuapp.com',
                        'http://custom-registry.com'
                    ]
                }
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                var found = results.some(function (entry) {
                    return entry.name === self.pkg;
                });

                expect(found).to.be(true);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.search(this.pkg, function (err, results) {
                if (! results.length) {
                    return next(new Error('Result expected'));
                }

                var found = results.some(function (entry) {
                    return entry.url === self.pkgUrl;
                });

                expect(found).to.be(true);
                next();
            });
        });
    });

    describe('calling the search instance method without argument', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages/search/')
              .reply(404);
        });

        it('should return an error and no results', function (next) {
            this.registry.search('', function (err, results) {
                expect(err).to.be.an(Error);
                expect(results).to.be(undefined);
                next();
            });
        });
    });

    //
    // list
    //
    describe('calling the list instance method', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages')
              .reply(200, [], {});

            this.registry._config.force = true;
        });

        it('should not return an error', function (next) {
            this.registry.list(function (err) {
                expect(err).to.be(null);
                next();
            });
        });

        it('should return results array', function (next) {
            this.registry.list(function (err, results) {
                expect(results).to.be.an('array');
                next();
            });
        });

    });

    describe('calling the list instance method with two registries', function () {
        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages')
              .reply(200, []);

            nock('http://custom-registry.com')
              .get('/packages')
              .reply(200, [
                {
                    name: 'jquery',
                    url: 'git://github.com/bar/foo.git'
                }
            ]);

            this.registry = new RegistryClient({
                strictSsl: false,
                force: true,
                registry: {
                    search: [
                        'https://bower.herokuapp.com',
                        'http://custom-registry.com'
                    ]
                }
            });
        });

        it('should return entry name', function (next) {
            var self = this;

            this.registry.list(function (err, results) {
                var found = results.some(function (entry) {
                    return entry.name === self.pkg;
                });

                expect(found).to.be(true);
                next();
            });
        });

        it('should return entry url', function (next) {
            var self = this;

            this.registry.list(function (err, results) {
                if (! results.length) {
                    return next(new Error('Result expected'));
                }

                var found = results.some(function (entry) {
                    return entry.url === self.pkgUrl;
                });

                expect(found).to.be(true);
                next();
            });
        });
    });

    describe('calling the list instance method', function () {

        beforeEach(function () {
            nock('https://bower.herokuapp.com:443')
              .get('/packages')
              .reply(200, [], {});
        });

        it('should return an error and no results', function (next) {
            this.registry.list(function (err) {
                expect(err).to.be(null);
                next();
            });
        });
    });

    //
    // clearCache
    //
    describe('called the clearCache instance method with argument', function () {
        beforeEach(function () {
            this.pkg = 'jquery';
        });

        it('should not return an error', function (next) {
            this.registry.clearCache(this.pkg, function (err) {
                expect(err).to.be(null);
                next();
            });
        });
    });

    describe('called the clearCache instance method without argument', function () {
        it('should not return any errors and remove all cache items', function (next) {
            this.registry.clearCache(function (err) {
                expect(err).to.be(null);
                next();
            });
        });
    });
});
<|endoftext|>


<commit_before>'use strict';

class SvgSlides {
  constructor (options) {
    var self = this;

    this._options = {};
    Object.keys(SvgSlides.defaultOptions).forEach(function (key) {
      var value;

      if (options && options[key]) {
        value = options[key];
      } else {
        value = SvgSlides.defaultOptions[key];
      }

      self._options[key] = value;
    });

    console.log('Powered by d3 v' + d3.version);
    var svg = d3.select('svg');
    svg.attr('preserveAspectRatio', 'xMidYMid meet');
    svg.attr('width', '100%');
    svg.attr('height', '100%');

    this._slides = svg.selectAll(this.options.slideSelector)[0];

    this._slides = this._slides.sort(function compareSlides (a, b) {
      var sortKeyA = a[self.options.sortSlidesBy].toString();
      var sortKeyB = b[self.options.sortSlidesBy].toString();
      if (sortKeyA < sortKeyB) {
        return -1;
      } else if (sortKeyA > sortKeyB) {
        return 1;
      } else {
        return 0;
      }
    });

    if (this.slides.length > 0) {
      var slideIds = this.slides.map(function (slide) {
        return slide.id;
      });
      console.log(`Found the following slides: ${slideIds}`);

      slideIds.forEach(function (slideId) {
        var matchingSlides = self.slides.filter(function (slide) {
          return (slide.id === slideId);
        });
        if (matchingSlides.length > 1) {
          console.error(`Found duplicate slide id: ${slide.id}`);
        }
      });
    } else {
      console.error('Found no slides!');
    }

    this.slides.forEach(function (slide) {
      slide.addEventListener('click', onClickSlide);

      function onClickSlide (event) {
        self.currentSlideId = slide.id;
      }
    });

    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('hashchange', onHashChange);

    svg.on('wheel', onMouseWheel);

    // start presentation
    onHashChange();

    function onHashChange () {
      var slideId = self.currentSlideId;
      if (!slideId) {
        if (self.slides.length > 0) {
          self.currentSlideId = self.slides[0].id;
        } else {
          self.currentSlideId = 'overview';
        }
        return;
      }

      var viewBoxPattern = /viewBox=(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*)/;

      if (slideId === 'overview') {
        console.log(`Transitioning to overview..`);
        transitionTo(svg.node());
      } else if (viewBoxPattern.test(slideId)) {
        var match = viewBoxPattern.exec(slideId);
        var viewBox = {
          left: match[1],
          top: match[2],
          width: match[3],
          height: match[4]
        };
        console.log(`Setting viewBox to ${viewBox.left},${viewBox.top},${viewBox.width},${viewBox.height}...`);
        svg.attr('viewBox', `${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`);
      } else {
        console.log(`Transitioning to slide ${slideId}...`);
        var slideNode = d3.select('#' + slideId).node();
        transitionTo(slideNode);
      }
    }

    function onKeyDown (event) {
      var key = event.code || event.keyIdentifier;

      var keyBinding;
      if (self._keyBindings instanceof Map) {
        keyBinding = self._keyBindings.get(key);
      } else {
        keyBinding = self._keyBindings[key];
      }

      if (keyBinding) {
        keyBinding();
      } else {
        console.log(`No keybinding for ${key}`);
      }
    }

    function onMouseWheel () {
      var zoom;
      if (d3.event.wheelDelta > 0) {
        console.log('Zooming in');
        zoom = 0.8;
      } else {
        console.log('Zooming out');
        zoom = 1.25;
      }

      var oldViewBox = svg.attr('viewBox')
        .split(' ')
        .map(function (component) {
          return parseInt(component);
        });

      var mouse = d3.mouse(this);
      var viewBox = {
        left: mouse[0] - (mouse[0] - oldViewBox[0]) * zoom,
        top: mouse[1] - (mouse[1] - oldViewBox[1]) * zoom,
        width: oldViewBox[2] * zoom,
        height: oldViewBox[3] * zoom
      };
      setViewBox(viewBox);
    }

    function setViewBox (viewBox) {
      window.location.hash = `#viewBox=${viewBox.left},${viewBox.top},${viewBox.width},${viewBox.height}`;
    }

    function transitionTo (svgElement) {
      if (!svgElement) {
        return;
      }

      var boundingBox = svgElement.getBBox();
      var margin = {
        x: boundingBox.width / 100,
        y: boundingBox.height / 100
      };
      var viewBox = {
        left: boundingBox.x - margin.x,
        top: boundingBox.y - margin.y,
        width: boundingBox.width + 2 * margin.x,
        height: boundingBox.height + 2 * margin.y
      };

      console.log(`Setting viewBox to ${viewBox.left},${viewBox.top},${viewBox.width},${viewBox.height}...`);
      svg.transition()
        .attr('viewBox', `${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`);
    }
  }

  get currentSlideId () {
    var hash = window.location.hash || '';
    if (hash === '') {
      return null;
    } else {
      return hash.replace(/^#/, '');
    }
  }

  set currentSlideId (slideId) {
    window.location.hash = `#${slideId}`;
  }

  get currentSlideIndex () {
    var self = this;

    var currentSlides = this.slides.filter(function (slide) {
      return (slide.id === self.currentSlideId);
    });

    if (currentSlides.length === 0) {
      console.error(`No slide found with id "${this.currentSlideId}"!`);
      return;
    }

    if (currentSlides.length > 1) {
      console.error(`More than one slide found with id "${this.currentSlideId}", using the first one!`);
    }

    return this.slides.indexOf(currentSlides[0]);
  }

  get defaultKeyBindings () {
    var keyBindings = new Map();
    keyBindings.set('ArrowLeft', this.transitionToPreviousSlide.bind(this));
    keyBindings.set('Left', this.transitionToPreviousSlide.bind(this));
    keyBindings.set('ArrowRight', this.transitionToNextSlide.bind(this));
    keyBindings.set('Right', this.transitionToNextSlide.bind(this));
    keyBindings.set('Space', this.transitionToNextSlide.bind(this));
    keyBindings.set('End', this.transitionToLastSlide.bind(this));
    keyBindings.set('Home', this.transitionToFirstSlide.bind(this));
    keyBindings.set('Escape', this.transitionToOverview.bind(this));
    keyBindings.set('U+001B', this.transitionToOverview.bind(this));
    return keyBindings;
  }

  static get defaultOptions () {
    return {
      slideSelector: '[id^=slide_]',
      sortSlidesBy: 'id'
    };
  }

  set keyBindings (newKeyBindings) {
    this._keyBindings = newKeyBindings;
  }

  static load () {
    window.addEventListener('load', function () {
      var svgSlides = new SvgSlides();
      svgSlides.keyBindings = svgSlides.defaultKeyBindings;
    });
  }

  get options () {
    return this._options;
  }

  get slides () {
    return this._slides;
  }

  transitionToFirstSlide () {
    this.currentSlideId = this.slides[0].id;
  }

  transitionToLastSlide () {
    this.currentSlideId = this.slides[this.slides.length - 1].id;
  }

  transitionToNextSlide () {
    if (this.currentSlideIndex < this.slides.length - 1) {
      this.currentSlideId = this.slides[this.currentSlideIndex + 1].id;
    }
  }

  transitionToOverview () {
    this.currentSlideId = 'overview';
  }

  transitionToPreviousSlide () {
    if (this.currentSlideIndex > 0) {
      this.currentSlideId = this.slides[this.currentSlideIndex - 1].id;
      return this.currentSlideId;
    }
  }
}
<commit_msg>transition to first slide when id is not found<commit_after>'use strict';

class SvgSlides {
  constructor (options) {
    var self = this;

    this._options = {};
    Object.keys(SvgSlides.defaultOptions).forEach(function (key) {
      var value;

      if (options && options[key]) {
        value = options[key];
      } else {
        value = SvgSlides.defaultOptions[key];
      }

      self._options[key] = value;
    });

    console.log('Powered by d3 v' + d3.version);
    var svg = d3.select('svg');
    svg.attr('preserveAspectRatio', 'xMidYMid meet');
    svg.attr('width', '100%');
    svg.attr('height', '100%');

    this._slides = svg.selectAll(this.options.slideSelector)[0];

    this._slides = this._slides.sort(function compareSlides (a, b) {
      var sortKeyA = a[self.options.sortSlidesBy].toString();
      var sortKeyB = b[self.options.sortSlidesBy].toString();
      if (sortKeyA < sortKeyB) {
        return -1;
      } else if (sortKeyA > sortKeyB) {
        return 1;
      } else {
        return 0;
      }
    });

    if (this.slides.length > 0) {
      var slideIds = this.slides.map(function (slide) {
        return slide.id;
      });
      console.log(`Found the following slides: ${slideIds}`);

      slideIds.forEach(function (slideId) {
        var matchingSlides = self.slides.filter(function (slide) {
          return (slide.id === slideId);
        });
        if (matchingSlides.length > 1) {
          console.error(`Found duplicate slide id: ${slide.id}`);
        }
      });
    } else {
      console.error('Found no slides!');
    }

    this.slides.forEach(function (slide) {
      slide.addEventListener('click', onClickSlide);

      function onClickSlide (event) {
        self.currentSlideId = slide.id;
      }
    });

    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('hashchange', onHashChange);

    svg.on('wheel', onMouseWheel);

    // start presentation
    onHashChange();

    function onHashChange () {
      var slideId = self.currentSlideId;
      if (!slideId || (self.currentSlideIndex < 0)) {
        if (self.slides.length > 0) {
          self.currentSlideId = self.slides[0].id;
        } else {
          self.currentSlideId = 'overview';
        }
        return;
      }

      var viewBoxPattern = /viewBox=(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*)/;

      if (slideId === 'overview') {
        console.log(`Transitioning to overview..`);
        transitionTo(svg.node());
      } else if (viewBoxPattern.test(slideId)) {
        var match = viewBoxPattern.exec(slideId);
        var viewBox = {
          left: match[1],
          top: match[2],
          width: match[3],
          height: match[4]
        };
        console.log(`Setting viewBox to ${viewBox.left},${viewBox.top},${viewBox.width},${viewBox.height}...`);
        svg.attr('viewBox', `${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`);
      } else {
        console.log(`Transitioning to slide ${slideId}...`);
        var slideNode = d3.select('#' + slideId).node();
        transitionTo(slideNode);
      }
    }

    function onKeyDown (event) {
      var key = event.code || event.keyIdentifier;

      var keyBinding;
      if (self._keyBindings instanceof Map) {
        keyBinding = self._keyBindings.get(key);
      } else {
        keyBinding = self._keyBindings[key];
      }

      if (keyBinding) {
        keyBinding();
      } else {
        console.log(`No keybinding for ${key}`);
      }
    }

    function onMouseWheel () {
      var zoom;
      if (d3.event.wheelDelta > 0) {
        console.log('Zooming in');
        zoom = 0.8;
      } else {
        console.log('Zooming out');
        zoom = 1.25;
      }

      var oldViewBox = svg.attr('viewBox')
        .split(' ')
        .map(function (component) {
          return parseInt(component);
        });

      var mouse = d3.mouse(this);
      var viewBox = {
        left: mouse[0] - (mouse[0] - oldViewBox[0]) * zoom,
        top: mouse[1] - (mouse[1] - oldViewBox[1]) * zoom,
        width: oldViewBox[2] * zoom,
        height: oldViewBox[3] * zoom
      };
      setViewBox(viewBox);
    }

    function setViewBox (viewBox) {
      window.location.hash = `#viewBox=${viewBox.left},${viewBox.top},${viewBox.width},${viewBox.height}`;
    }

    function transitionTo (svgElement) {
      if (!svgElement) {
        return;
      }

      var boundingBox = svgElement.getBBox();
      var margin = {
        x: boundingBox.width / 100,
        y: boundingBox.height / 100
      };
      var viewBox = {
        left: boundingBox.x - margin.x,
        top: boundingBox.y - margin.y,
        width: boundingBox.width + 2 * margin.x,
        height: boundingBox.height + 2 * margin.y
      };

      console.log(`Setting viewBox to ${viewBox.left},${viewBox.top},${viewBox.width},${viewBox.height}...`);
      svg.transition()
        .attr('viewBox', `${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`);
    }
  }

  get currentSlideId () {
    var hash = window.location.hash || '';
    if (hash === '') {
      return null;
    } else {
      return hash.replace(/^#/, '');
    }
  }

  set currentSlideId (slideId) {
    window.location.hash = `#${slideId}`;
  }

  get currentSlideIndex () {
    var self = this;

    var currentSlides = this.slides.filter(function (slide) {
      return (slide.id === self.currentSlideId);
    });

    if (currentSlides.length === 0) {
      console.error(`No slide found with id "${this.currentSlideId}"!`);
      return -1;
    }

    if (currentSlides.length > 1) {
      console.error(`More than one slide found with id "${this.currentSlideId}", using the first one!`);
    }

    return this.slides.indexOf(currentSlides[0]);
  }

  get defaultKeyBindings () {
    var keyBindings = new Map();
    keyBindings.set('ArrowLeft', this.transitionToPreviousSlide.bind(this));
    keyBindings.set('Left', this.transitionToPreviousSlide.bind(this));
    keyBindings.set('ArrowRight', this.transitionToNextSlide.bind(this));
    keyBindings.set('Right', this.transitionToNextSlide.bind(this));
    keyBindings.set('Space', this.transitionToNextSlide.bind(this));
    keyBindings.set('End', this.transitionToLastSlide.bind(this));
    keyBindings.set('Home', this.transitionToFirstSlide.bind(this));
    keyBindings.set('Escape', this.transitionToOverview.bind(this));
    keyBindings.set('U+001B', this.transitionToOverview.bind(this));
    return keyBindings;
  }

  static get defaultOptions () {
    return {
      slideSelector: '[id^=slide_]',
      sortSlidesBy: 'id'
    };
  }

  set keyBindings (newKeyBindings) {
    this._keyBindings = newKeyBindings;
  }

  static load () {
    window.addEventListener('load', function () {
      var svgSlides = new SvgSlides();
      svgSlides.keyBindings = svgSlides.defaultKeyBindings;
    });
  }

  get options () {
    return this._options;
  }

  get slides () {
    return this._slides;
  }

  transitionToFirstSlide () {
    this.currentSlideId = this.slides[0].id;
  }

  transitionToLastSlide () {
    this.currentSlideId = this.slides[this.slides.length - 1].id;
  }

  transitionToNextSlide () {
    if (this.currentSlideIndex < this.slides.length - 1) {
      this.currentSlideId = this.slides[this.currentSlideIndex + 1].id;
    }
  }

  transitionToOverview () {
    this.currentSlideId = 'overview';
  }

  transitionToPreviousSlide () {
    if (this.currentSlideIndex > 0) {
      this.currentSlideId = this.slides[this.currentSlideIndex - 1].id;
      return this.currentSlideId;
    }
  }
}
<|endoftext|>