Skip to content

Instantly share code, notes, and snippets.

@pbojinov
Last active July 27, 2018 19:18

Revisions

  1. pbojinov revised this gist Jul 27, 2018. 1 changed file with 68 additions and 0 deletions.
    68 changes: 68 additions & 0 deletions content-body-rest.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,68 @@
    // https://bitbucket.org/atlassian/confluence-react-components/src/master/src/content-body-rest/content-body-rest.js

    import React, { Component, PropTypes } from 'react';
    import { createElement } from '../facades/document';

    export default class ContentBodyRest extends Component {
    constructor() {
    super();
    this._setScriptContainerRef = this._setScriptContainerRef.bind(this);
    this._setBodyContainerRef = this._setBodyContainerRef.bind(this);
    }

    componentDidMount() {
    this._updateAnchorTargets();
    this._loadJSDependencies();
    }

    componentDidUpdate() {
    this._updateAnchorTargets();
    this._loadJSDependencies();
    }

    _updateAnchorTargets() {
    const anchors = this._bodyContainer.querySelectorAll('a');
    for (let i = 0, anchor; anchor = anchors[i]; i++) {
    anchor.target = '_top';
    }
    }

    _loadJSDependencies() {
    const content = this.props.content;
    content.jsDependencies.forEach(this._appendScriptToContainer, this);
    }

    _appendScriptToContainer(uri) {
    const scriptElement = createElement('script');
    scriptElement.async = true;
    scriptElement.src = uri;
    this._scriptContainer.appendChild(scriptElement);
    }

    _setBodyContainerRef(node) {
    this._bodyContainer = node;
    }

    _setScriptContainerRef(node) {
    this._scriptContainer = node;
    }

    render() {
    const { body, cssDependencies } = this.props.content;

    return (
    <div id="content" className="page view">
    <div dangerouslySetInnerHTML={{ __html: `${body}${cssDependencies}` }} ref={this._setBodyContainerRef} id="main-content" className="wiki-content"></div>
    <div ref={this._setScriptContainerRef}></div>
    </div>
    );
    }
    }

    ContentBodyRest.displayName = 'ContentBodyRest';
    ContentBodyRest.propTypes = {
    /**
    * The ID of the content to render.
    */
    contentId: PropTypes.string
    };
  2. pbojinov revised this gist Jul 27, 2018. 1 changed file with 129 additions and 0 deletions.
    129 changes: 129 additions & 0 deletions content-body-iframe-test.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,129 @@
    import React from 'react';

    import sinon, { spy, stub } from 'sinon';
    import sinonChai from 'sinon-chai';
    import { shallow, render } from 'enzyme';
    import chai, { expect } from 'chai';
    import chaiEnzyme from 'chai-enzyme';
    import proxyquire from 'proxyquire';

    chai.use(chaiEnzyme());
    chai.use(sinonChai);

    const WINDOW = '../facades/window';
    const UUID = '../util/uuid';

    describe('ContentBodyIframe', () => {
    let mocks;
    let ContentBodyIframe;

    beforeEach(() => {
    mocks = {
    [UUID]: {
    default: sinon.stub()
    },
    [WINDOW]: {
    addEventListener: sinon.stub(),
    removeEventListener: sinon.stub()
    }
    };

    ContentBodyIframe = proxyquire('../content-body-iframe', mocks).default;
    });

    describe('render', () => {
    let wrapper;
    const uuid = 'foobar!';
    const id = '123';
    const props = {
    baseUrl: 'http://www.atlassian.com',
    content: {id},
    onContentLoaded: sinon.spy()
    };

    beforeEach(() => {
    mocks[UUID].default.returns(uuid);
    wrapper = shallow(<ContentBodyIframe {...props} />);
    });

    it('Should render a iframe', () => {
    expect(wrapper).to.have.descendants('iframe');

    const iframe = wrapper.find('iframe');
    expect(iframe).to.have.prop('src').equal(`${props.baseUrl}/confluence/content-only/viewpage.action?pageId=${id}&iframeId=${uuid}`);
    expect(iframe).to.have.prop('onLoad').equal(props.onContentLoaded);
    });

    it('if `contextPath` prop not passed it is "/confluence"', () => {
    expect(wrapper.find('iframe')).to.have.prop('src').equal(`${props.baseUrl}/confluence/content-only/viewpage.action?pageId=${id}&iframeId=${uuid}`);
    });

    it('use passed `contextPath` for iframe', () => {
    wrapper.setProps({contextPath: '/wiki'});
    expect(wrapper.find('iframe')).to.have.prop('src').equal(`${props.baseUrl}/wiki/content-only/viewpage.action?pageId=${id}&iframeId=${uuid}`);

    wrapper.setProps({contextPath: ''});
    expect(wrapper.find('iframe')).to.have.prop('src').equal(`${props.baseUrl}/content-only/viewpage.action?pageId=${id}&iframeId=${uuid}`);
    });
    });

    describe('componentDidMount', () => {
    let wrapper;
    let instance;
    let uuid;

    beforeEach(() => {
    uuid = 'barfoo';
    mocks[UUID].default.returns(uuid);
    wrapper = shallow(<ContentBodyIframe baseUrl="" content={{}}/>);
    instance = wrapper.instance();
    instance.componentDidMount();
    });

    it('Should setup a message event listener', () => {
    expect(mocks[WINDOW].addEventListener).to.have.been.calledWith('message', instance.receiveMessage);
    });

    describe('receiveMessage handler', () => {
    let receiveMessage;
    beforeEach(() => {
    receiveMessage = mocks[WINDOW].addEventListener.getCall(0).args[1];
    });

    describe(`When iframeId doesn't match the iframeId of the component`, () => {
    it('Should return undefined', () => {
    expect(receiveMessage({
    data: {
    iframeId: 'something',
    height: 1234
    }
    })).to.equal();
    });
    });

    describe('When event iframeId matches the iframeId of the component', () => {
    it('Should setState with the height in the message', () => {
    const height = 599;
    receiveMessage({
    data: {
    iframeId: uuid,
    height
    }
    });

    expect(wrapper).to.have.state('height', `${height}px`);
    });
    });
    });

    describe('componentWillUnmount', () => {
    beforeEach(() => {
    instance.componentWillUnmount();
    });

    it('Should remove the message event listener', () => {
    expect(mocks[WINDOW].removeEventListener).to.have.been.calledWith('message', instance.receiveMessage);
    });
    });
    });
    });
  3. pbojinov created this gist Jul 27, 2018.
    85 changes: 85 additions & 0 deletions content-body-iframe.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,85 @@
    import React, { Component, PropTypes } from 'react';
    import uuid from '../util/uuid';
    import { addEventListener, removeEventListener } from '../facades/window';

    const DEFAULT_CONTEXT_PATH = '/confluence';

    export default class ContentBodyIframe extends Component {
    constructor() {
    super();

    this.iframeId = uuid();

    this.state = {
    height: '1000px'
    };
    }

    componentDidMount() {
    this.receiveMessage = event => {
    const { iframeId } = event.data;

    if (!iframeId || iframeId !== this.iframeId) {
    return;
    }

    const { height } = event.data;

    this.setState({
    height: `${height}px`
    });
    };

    addEventListener('message', this.receiveMessage);
    }

    componentWillUnmount() {
    removeEventListener('message', this.receiveMessage);
    }

    render() {
    const { content, baseUrl, onContentLoaded } = this.props;
    let { contextPath } = this.props;
    const { height } = this.state;

    if (typeof contextPath === 'undefined') {
    contextPath = DEFAULT_CONTEXT_PATH;
    }

    const contentUrl = `${baseUrl}${contextPath}/content-only/viewpage.action?pageId=${content.id}&iframeId=${this.iframeId}`;

    return (
    <iframe
    src={contentUrl}
    border="0"
    style={{border:0, width: '100%', height}}
    onLoad={onContentLoaded}
    />
    );
    }
    }

    ContentBodyIframe.displayName = 'ContentBodyIframe';

    ContentBodyIframe.defaultProps = {
    baseUrl: ''
    };

    ContentBodyIframe.propTypes = {
    /**
    * The ID of the content to render.
    */
    contentId: PropTypes.string,
    /**
    * Host of confluence instance for the iframe src attribute
    */
    baseUrl: PropTypes.string,
    /**
    * Confluence instance context path
    */
    contextPath: PropTypes.string,
    /**
    * Callback when iframe finishes loading.
    */
    onContentLoaded: PropTypes.func
    };