├── .babelrc ├── .dockerignore ├── .env ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .nvmrc ├── .stylelintrc.json ├── .travis.yml ├── Dockerfile ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PRIVACY_POLICY.md ├── README.md ├── Vagrantfile ├── ci.sh ├── docker-compose.yml ├── download └── README ├── entrypoint.sh ├── jenkins.sh ├── nodemon.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── private └── res │ ├── banner-bottom-black.jpg │ ├── banner.jpg │ ├── icon_biofactoid_circle.svg │ ├── icon_biofactoid_filled.svg │ ├── icon_biofactoid_outline.svg │ ├── logo-horizontal-with-text.svg │ ├── logo-no-tagline.svg │ ├── logos.pdf │ ├── logos.svg │ ├── shield_harvard.png │ ├── shield_ohsu.png │ └── shield_uoft.png ├── public ├── bundle.css ├── bundle.js ├── deps.css ├── deps.js ├── image ├── polyfills.js └── webpackjsonp.js ├── src ├── client-env-vars.json ├── client │ ├── components │ │ ├── accordion.js │ │ ├── animate.js │ │ ├── carousel.js │ │ ├── citation.js │ │ ├── copy-field.js │ │ ├── data-component.js │ │ ├── dirty-component.js │ │ ├── document-linkout.js │ │ ├── document-management-components.js │ │ ├── document-management.js │ │ ├── document-seeder-example.js │ │ ├── document-seeder.js │ │ ├── editor │ │ │ ├── buttons.js │ │ │ ├── caption.js │ │ │ ├── cy │ │ │ │ ├── automove.js │ │ │ │ ├── compound-dnd.js │ │ │ │ ├── cxtmenu.js │ │ │ │ ├── defs.js │ │ │ │ ├── doc.js │ │ │ │ ├── edgehandles.js │ │ │ │ ├── index.js │ │ │ │ ├── layout.js │ │ │ │ ├── on-key.js │ │ │ │ ├── stylesheet.js │ │ │ │ ├── tippy.js │ │ │ │ └── viewport.js │ │ │ ├── defs.js │ │ │ ├── explore-share.js │ │ │ ├── help.js │ │ │ ├── index.js │ │ │ ├── info-panel.js │ │ │ ├── submit.js │ │ │ ├── title.js │ │ │ └── undo-remove.js │ │ ├── element-info │ │ │ ├── chemical-formula.js │ │ │ ├── element-info.js │ │ │ ├── entity-assoc-display.js │ │ │ ├── entity-info.js │ │ │ ├── interaction-info.js │ │ │ ├── participant-info.js │ │ │ ├── progression-stepper.js │ │ │ └── progression.js │ │ ├── highlighter.js │ │ ├── home.js │ │ ├── main-menu.js │ │ ├── native-share.js │ │ ├── notification │ │ │ ├── base.js │ │ │ ├── corner.js │ │ │ ├── index.js │ │ │ ├── inline.js │ │ │ ├── list.js │ │ │ ├── notification.js │ │ │ ├── panel.js │ │ │ └── popover.js │ │ ├── page-not-found.js │ │ ├── popover │ │ │ ├── popover.js │ │ │ └── tooltip.js │ │ ├── related-papers.js │ │ ├── request-form.js │ │ ├── tasks.js │ │ ├── toggle.js │ │ └── twitter-share.js │ ├── debug.js │ ├── defs.js │ ├── document-search.js │ ├── dom.js │ ├── index.js │ ├── logger.js │ ├── polyfills.js │ └── router.js ├── config.js ├── model │ ├── document │ │ ├── document.js │ │ └── index.js │ ├── element-cache.js │ ├── element-set.js │ ├── element │ │ ├── complex.js │ │ ├── element-type.js │ │ ├── element.js │ │ ├── entity-type.js │ │ ├── entity.js │ │ ├── factory.js │ │ ├── index.js │ │ ├── interaction-type │ │ │ ├── binding.js │ │ │ ├── biopax-type.js │ │ │ ├── demethylation.js │ │ │ ├── dephosphorylation.js │ │ │ ├── deubiquitination.js │ │ │ ├── enum.js │ │ │ ├── index.js │ │ │ ├── interaction-type.js │ │ │ ├── interaction.js │ │ │ ├── methylation.js │ │ │ ├── modification.js │ │ │ ├── phosphorylation.js │ │ │ ├── transcription-translation.js │ │ │ └── ubiquitination.js │ │ ├── interaction.js │ │ └── participant-type.js │ ├── empty-element-cache.js │ ├── event-emitter-mixin.js │ ├── hint.js │ ├── organism.js │ └── syncher.js ├── neo4j │ ├── index.js │ ├── neo4j-document.js │ ├── neo4j-driver.js │ ├── neo4j-functions.js │ ├── query-strings.js │ └── test-functions.js ├── server │ ├── db.js │ ├── email-transport.js │ ├── email.js │ ├── index.js │ ├── logger.js │ ├── routes │ │ ├── api │ │ │ ├── document │ │ │ │ ├── cache.js │ │ │ │ ├── crossref │ │ │ │ │ ├── api.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── map.js │ │ │ │ │ └── works.js │ │ │ │ ├── export.js │ │ │ │ ├── filters.js │ │ │ │ ├── graphdb.js │ │ │ │ ├── hint │ │ │ │ │ └── pubtator.js │ │ │ │ ├── index.js │ │ │ │ ├── indra.js │ │ │ │ ├── null.js │ │ │ │ ├── p-limit.js │ │ │ │ ├── pubmed │ │ │ │ │ ├── demoPubmedArticle.js │ │ │ │ │ ├── fetchPubmed.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── linkPubmed.js │ │ │ │ │ └── searchPubmed.js │ │ │ │ ├── reach.js │ │ │ │ ├── related-papers-queue.js │ │ │ │ └── update.js │ │ │ ├── element-association │ │ │ │ ├── grounding-search.js │ │ │ │ ├── index.js │ │ │ │ └── pc.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── socket.io.js │ │ └── style-demo.js │ ├── sitemap.js │ └── update-cron.js ├── styles │ ├── accordion.css │ ├── animations.css │ ├── app.css │ ├── base.css │ ├── carousel.css │ ├── citation.css │ ├── copy-field.css │ ├── custom-icons.css │ ├── deps.css │ ├── document-linkout.css │ ├── document-management.css │ ├── document-preview.css │ ├── document-seeder.css │ ├── editor │ │ ├── cxtmenu.css │ │ ├── editor.css │ │ ├── index.css │ │ └── undo-remove.css │ ├── element-info │ │ ├── element-info.css │ │ ├── entity-info.css │ │ ├── index.css │ │ ├── interaction-info.css │ │ └── participant-info.css │ ├── form-editor │ │ ├── form-editor.css │ │ └── index.css │ ├── help.css │ ├── highlighter.css │ ├── home.css │ ├── image │ │ ├── arrow-negative-small-white.svg │ │ ├── arrow-negative-small.svg │ │ ├── arrow-negative.svg │ │ ├── arrow-positive-filled.svg │ │ ├── arrow-positive-long-filled-white.svg │ │ ├── arrow-positive-long-filled.svg │ │ ├── arrow-positive-long.svg │ │ ├── arrow-positive-small.svg │ │ ├── arrow-positive.svg │ │ ├── arrow-unsigned-white.svg │ │ ├── arrow-unsigned.svg │ │ ├── banner-puzzle-missing.jpg │ │ ├── banner-puzzle.jpg │ │ ├── banner.jpg │ │ ├── banner.png │ │ ├── card-banner.png │ │ ├── example-doc-1.png │ │ ├── example-doc-2.png │ │ ├── fa-angle-down.svg │ │ ├── home-figure-1.jpg │ │ ├── home-figure-2-slim.png │ │ ├── home-figure-2.png │ │ ├── home-figure-fg-2.png │ │ ├── home-figure-old-2.jpg │ │ ├── home-figure-old-3.jpg │ │ ├── ic_data_usage_black_24px.svg │ │ ├── ic_keyboard_tab_24px.svg │ │ ├── ic_person_pin_black_24px.svg │ │ ├── icon-twitter-white.svg │ │ ├── icon-twitter.svg │ │ ├── layer-cake-2.png │ │ ├── logo-horizontal-name-only-black.svg │ │ ├── logo-horizontal-name-only-white.svg │ │ ├── logo-horizontal-no-text.svg │ │ ├── logo-horizontal-text-only-white.svg │ │ ├── logo-horizontal-text-only-yellow.svg │ │ ├── logo-horizontal-text-only.svg │ │ ├── logo-horizontal-with-text.svg │ │ ├── logo-no-circle.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo_harvardu.png │ │ ├── logo_ohsu.jpg │ │ ├── logo_utoronto.png │ │ ├── macbookgrey_front.png │ │ ├── macbookpro15_front.png │ │ ├── mol-cell-logo.svg │ │ ├── orcid-icon.svg │ │ ├── phone-explore-1.png │ │ ├── phone-explore-2.png │ │ ├── phone-explore-3.png │ │ ├── sample-editor-screen-fade-to-white.mp4 │ │ ├── sample-editor-screen-fade.mp4 │ │ ├── sample-editor-screen-ras-raf.mp4 │ │ ├── sample-editor-screen.m4v │ │ ├── share-white.svg │ │ ├── share.svg │ │ ├── spinner.svg │ │ ├── welcome-aboard-1.svg │ │ ├── welcome-aboard-2.svg │ │ ├── welcome-aboard-3.svg │ │ └── welcome-aboard-4.svg │ ├── index.css │ ├── init-app.css │ ├── main-menu.css │ ├── native-share.css │ ├── notification.css │ ├── popover │ │ ├── index.css │ │ ├── popover.css │ │ ├── tippy.css │ │ └── tooltip.css │ ├── react-tabs.css │ ├── related-papers.css │ ├── request-form.css │ ├── tasks.css │ ├── twitter-share.css │ ├── vars.css │ └── vendor │ │ ├── bio-icons.css │ │ ├── font-awesome-custom.css │ │ ├── font-awesome-form.css │ │ ├── font-awesome.css │ │ ├── fonts │ │ ├── bio-icons │ │ │ ├── bio-icons.eot │ │ │ ├── bio-icons.svg │ │ │ ├── bio-icons.ttf │ │ │ └── bio-icons.woff │ │ ├── font-awesome │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── inconsolata-ascii │ │ │ ├── inconsolata-webfont.woff │ │ │ └── inconsolata-webfont.woff2 │ │ ├── inconsolata │ │ │ ├── generator_config.txt │ │ │ ├── inconsolata.woff │ │ │ └── inconsolata.woff2 │ │ ├── material-icons │ │ │ ├── MaterialIcons-Regular.woff │ │ │ └── MaterialIcons-Regular.woff2 │ │ ├── open-sans-latin-greek │ │ │ ├── generator_config.txt │ │ │ ├── opensans-bold-webfont.woff │ │ │ ├── opensans-bold-webfont.woff2 │ │ │ ├── opensans-bolditalic-webfont.woff │ │ │ ├── opensans-bolditalic-webfont.woff2 │ │ │ ├── opensans-extrabold-webfont.woff │ │ │ ├── opensans-extrabold-webfont.woff2 │ │ │ ├── opensans-extrabolditalic-webfont.woff │ │ │ ├── opensans-extrabolditalic-webfont.woff2 │ │ │ ├── opensans-italic-webfont.woff │ │ │ ├── opensans-italic-webfont.woff2 │ │ │ ├── opensans-light-webfont.woff │ │ │ ├── opensans-light-webfont.woff2 │ │ │ ├── opensans-lightitalic-webfont.woff │ │ │ ├── opensans-lightitalic-webfont.woff2 │ │ │ ├── opensans-regular-webfont.woff │ │ │ ├── opensans-regular-webfont.woff2 │ │ │ ├── opensans-semibold-webfont.woff │ │ │ ├── opensans-semibold-webfont.woff2 │ │ │ ├── opensans-semibolditalic-webfont.woff │ │ │ └── opensans-semibolditalic-webfont.woff2 │ │ └── open-sans │ │ │ ├── generator_config.txt │ │ │ ├── opensans-bold.woff │ │ │ ├── opensans-bold.woff2 │ │ │ ├── opensans-bolditalic.woff │ │ │ ├── opensans-bolditalic.woff2 │ │ │ ├── opensans-italic.woff │ │ │ ├── opensans-italic.woff2 │ │ │ ├── opensans-light.woff │ │ │ ├── opensans-light.woff2 │ │ │ ├── opensans-lightitalic.woff │ │ │ ├── opensans-lightitalic.woff2 │ │ │ ├── opensans-regular.woff │ │ │ ├── opensans-regular.woff2 │ │ │ ├── opensans-semibold.woff │ │ │ ├── opensans-semibold.woff2 │ │ │ ├── opensans-semibolditalic.woff │ │ │ └── opensans-semibolditalic.woff2 │ │ ├── inconsolata-ascii.css │ │ ├── inconsolata.css │ │ ├── material-form.css │ │ ├── material-icons.css │ │ ├── open-sans-latin-greek.css │ │ └── open-sans.css ├── util │ ├── article.js │ ├── assert.js │ ├── cache.js │ ├── cy.js │ ├── fetch.js │ ├── index.js │ ├── is.js │ ├── memoize.js │ ├── obj.js │ ├── promise.js │ ├── pubmed.js │ ├── registry.js │ ├── strings.js │ └── time.js └── views │ ├── 404.ejs │ ├── error.html │ ├── index.html.ejs │ └── style-demo.html ├── test ├── biopax │ ├── biopax.js │ └── sampleTemplate.json ├── crossRef │ ├── 10.1016_j.molcel.2019.06.008.json │ ├── 10.1101_2022.09.28.22280453.json │ ├── 10.1101_2023.04.12.536510.json │ ├── 10.3030_101067344.json │ ├── 10.7554_eLife.68292.json │ ├── 10.7554_eLife.87468.2.json │ ├── 10.7554_eLife.87468.2_noabstract.json │ ├── 10.7554_elife.86689.3.json │ ├── work_query_1.json │ ├── work_query_2.json │ ├── work_query_3.json │ ├── work_query_4.json │ ├── work_query_5.json │ └── works.js ├── crossref │ └── map.js ├── data │ └── document.json ├── document.js ├── element.js ├── entity.js ├── interaction.js ├── mock │ ├── cache.js │ └── socket.js ├── neo4j-test │ ├── add-nodes-edges.js │ ├── connect-neo4j.js │ ├── doc-tests-complex.js │ ├── doc-tests.js │ ├── document │ │ ├── complex_tests_1.json │ │ ├── complex_tests_2.json │ │ ├── complex_tests_3.json │ │ ├── complex_tests_4.json │ │ ├── complex_tests_5.json │ │ ├── complex_tests_6.json │ │ ├── doct_tests_1.json │ │ ├── doct_tests_2.json │ │ ├── doct_tests_3.json │ │ ├── doct_tests_4.json │ │ ├── doct_tests_5.json │ │ └── testDoc.json │ ├── fixtures.js │ └── search-tests.js ├── pubmed │ ├── fetchPubmed.js │ ├── pmid_151222.xml │ ├── pmid_29440426.xml │ ├── pmid_30078747.xml │ ├── pmid_30115697.xml │ ├── pmid_31511694.xml │ ├── pmid_38289659.xml │ ├── pmid_9417067.xml │ ├── pmid_9500320.xml │ ├── searchPubmed.js │ └── searchPubmedData.js ├── pubtator │ ├── 10.1016_j.molcel.2016.11.034.json │ ├── 10.1016_j.molcel.2019.03.023.json │ ├── 10.1016_j.molcel.2019.04.005.json │ ├── 10.1016_j.molcel.2024.01.007.json │ ├── 10.1038_s41556-021-00642-9.json │ ├── 10.1126_scisignal.abf3535.json │ ├── 10.15252_embj.2023113616.json │ ├── pubtator.js │ └── pubtator_8.json ├── sitemap │ ├── docs2Sitemap.js │ └── docsData.json ├── syncher.js └── util │ ├── article.js │ ├── conf.js │ ├── socket-io.js │ ├── table.js │ └── when.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | GRAPHDB_IMAGE_TAG=5.4.0 2 | RETHINKDB_IMAGE_TAG=latest 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | build/* 3 | src/server/routes/api/document/p-limit.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 8 16 | }, 17 | "plugins": [ 18 | 19 | ], 20 | "rules": { 21 | "semi": "error", 22 | "no-console": "warn", 23 | "no-unused-vars": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | /build 5 | /rethinkdb_data 6 | .vagrant 7 | *.log 8 | /.idea/ 9 | *.iml 10 | /download/* 11 | !/download/README 12 | .vscode -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-options=--max_old_space_size=4096 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.19.0 2 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreFiles": [ 3 | "./node_modules/**/*", 4 | "./src/styles/vendor/**/*", 5 | "./build/*", 6 | "**/*.js" 7 | ], 8 | "extends": "stylelint-config-standard", 9 | "rules": { 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - "10" 5 | - "12" 6 | #addons: 7 | # rethinkdb: '2.3.6' 8 | #before_script: 9 | # - sudo service rethinkdb start 10 | # - sleep 60 11 | script: npm run test:travis 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.17.0 2 | 3 | # Allow user configuration of variable at build-time using --build-arg flag 4 | # See src/client-env-vars.json 5 | ARG NODE_ENV 6 | ARG BASE_URL 7 | 8 | # Initialize environment and override with build-time flag, if set 9 | ENV NODE_ENV ${NODE_ENV:-production} 10 | 11 | # Create an unprivileged user w/ home directory 12 | RUN groupadd appuser \ 13 | && useradd --gid appuser --shell /bin/bash --create-home appuser 14 | 15 | # Create app directory 16 | RUN mkdir -p /home/appuser/app 17 | WORKDIR /home/appuser/app 18 | 19 | # Copy in source code 20 | COPY . /home/appuser/app 21 | 22 | # Install app dependencies 23 | # Puppeteer requirements 24 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 25 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 26 | && apt-get update \ 27 | && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 libxtst6 gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \ 28 | --no-install-recommends \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | # Note: NODE_ENV is development so that dev deps are installed 32 | RUN NODE_ENV=development npm install 33 | 34 | # Expose port 35 | EXPOSE 3000 36 | 37 | # Change ownership of the app to the unprivileged user 38 | RUN chown appuser:appuser -R /home/appuser/app 39 | USER appuser 40 | 41 | # Apply start commands 42 | COPY entrypoint.sh / 43 | CMD ["/entrypoint.sh"] 44 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | **Q: What is the name of the feature?** 6 | 7 | A: 8 | 9 | **Q: What does this feature enable the user to do?** 10 | 11 | A: 12 | 13 | **Q: What information must the user provide to use the feature?** 14 | 15 | A: 16 | 17 | **Q: What are the applicable constraints, e.g. compatibility or performance?** 18 | 19 | A: 20 | 21 | **Q: How does this feature affect each class of user (persona)?** 22 | 23 | A: 24 | 25 | - Biologist: 26 | - Editor: 27 | - Computational biologist: 28 | - Curator: 29 | 30 | 54 | 55 | ## Specification 56 | 57 | ### Mockup 58 | 59 | 60 | 61 | 62 | 63 | ### Details 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 - 2021 Pathway Commons 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a helper script to set up a very simple CI dev/testing server. It can 4 | # be used with `cron` in order to set up regular builds, e.g. for every 15 minutes: 5 | # 6 | # `crontab -e` 7 | # 8 | # @reboot /home/username/rethinkdb.sh > /home/username/rethinkdb.log 9 | # */15 * * * * /home/username/master.sh > /home/username/master.log 10 | # 11 | # To use this script, create a script per server instance, e.g. `master.sh`: 12 | # 13 | # #!/bin/bash 14 | # 15 | # # Mandatory repo/branch conf 16 | # export REPO=https://github.com/PathwayCommons/factoid.git 17 | # export BRANCH=master 18 | # export JOB_NAME=factoid-master 19 | # 20 | # # Project-specific env vars 21 | # export PORT=3000 22 | # 23 | # ./ci.sh 24 | 25 | echo "--" 26 | echo "Starting $JOB_NAME build on" 27 | date 28 | 29 | WORKSPACE=/home/`whoami`/$JOB_NAME 30 | WORKSPACE_TMP=/tmp/$JOB_NAME 31 | 32 | rm -rf $WORKSPACE_TMP 33 | mkdir -p $WORKSPACE_TMP 34 | cd $WORKSPACE_TMP 35 | 36 | # get the repo 37 | git clone $REPO $WORKSPACE_TMP 38 | git checkout $BRANCH 39 | 40 | # build 41 | npm install 42 | npm run clean || echo "No clean script found" 43 | 44 | export NODE_ENV=production 45 | 46 | npm run build || echo "No build script found" 47 | 48 | if [ $COMMAND ] 49 | then 50 | npm run $COMMAND 51 | fi 52 | 53 | # stop the old screen session 54 | echo "Quitting old screen session..." 55 | screen -S $JOB_NAME -X -p 0 stuff ^C && echo "Sent ^C" || echo "No screen session to ^C" 56 | screen -S $JOB_NAME -X quit && echo "Quit old screen session" || echo "No screen session to stop" 57 | 58 | #echo "Waiting a bit to let the old app exit..." 59 | #sleep 30 60 | 61 | # swap out old workspace with new one 62 | echo "Replacing workspace..." 63 | mkdir -p /tmp/rm 64 | mv $WORKSPACE /tmp/rm/$JOB_NAME && echo "Moved old workspace to /tmp/rm" || echo "No old workspace to move" 65 | mv $WORKSPACE_TMP $WORKSPACE 66 | cd $WORKSPACE 67 | echo "Replaced workspace" 68 | 69 | # start the server in a screen session 70 | echo "Starting new screen session..." 71 | screen -d -m -S $JOB_NAME bash -c "npm run ${START_SCRIPT:-start} 2>&1 | tee ~/$JOB_NAME.screen.log" 72 | echo "New screen session started" 73 | 74 | # delete the old workspace files 75 | echo "Deleting old workspace..." 76 | rm -rf /tmp/rm/$JOB_NAME && echo "Old workspace deleted" || echo "No old workspace to delete" 77 | 78 | echo "CI script complete" 79 | 80 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | graphdb: 4 | image: neo4j:${GRAPHDB_IMAGE_TAG:-latest} 5 | restart: unless-stopped 6 | container_name: graphdb 7 | ports: 8 | - "7474:7474" 9 | - "7687:7687" 10 | volumes: 11 | - graphdb-data:/data 12 | - graphdb-plugins:/plugins 13 | environment: 14 | NEO4J_AUTH: none 15 | NEO4J_apoc_export_file_enabled: true 16 | NEO4J_apoc_import_file_enabled: true 17 | NEO4J_apoc_import_file_use__neo4j__config: true 18 | NEO4J_PLUGINS: '["apoc"]' 19 | networks: 20 | - graphdb-config-network 21 | db: 22 | image: pathwaycommons/rethinkdb-docker:${RETHINKDB_IMAGE_TAG:-latest} 23 | restart: unless-stopped 24 | container_name: db 25 | ports: 26 | - "8080:8080" 27 | - "28015:28015" 28 | - "29015:29015" 29 | volumes: 30 | - db-data:/data 31 | networks: 32 | - graphdb-config-network 33 | 34 | volumes: 35 | graphdb-data: 36 | graphdb-plugins: 37 | db-data: 38 | 39 | networks: 40 | graphdb-config-network: 41 | driver: bridge -------------------------------------------------------------------------------- /download/README: -------------------------------------------------------------------------------- 1 | This folder is allocated to store the downloaded files. 2 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm run clean 3 | npm run build 4 | google-chrome-stable & 5 | cd /home/appuser/app && npm run start -------------------------------------------------------------------------------- /jenkins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run this script in a Jenkins project config for continuous integration 4 | # 5 | # E.g. in "Build" > "Execute shell", specify: 6 | # 7 | # export FOO="bar" # set up your env vars for the server 8 | # $WORKSPACE/jenkins.sh 9 | 10 | export BUILD_ID=dontKillMe 11 | 12 | cd $WORKSPACE 13 | 14 | # make sure we have all npm deps 15 | npm install 16 | 17 | # build 18 | export NODE_ENV=production 19 | npm run clean 20 | npm run build 21 | 22 | # move the build to a safe dir (won't be implicitly deleted by new build) 23 | mv $WORKSPACE $WORKSPACE-$BUILD_NUMBER 24 | cd $WORKSPACE-$BUILD_NUMBER 25 | 26 | # stop the server and make sure the old screen session is killed 27 | npm stop || echo "No npm task to stop" 28 | screen -X -S $JOB_NAME quit || echo "No screen session to stop" 29 | 30 | # start the server in a screen session so jenkins doesn't kill it 31 | screen -d -m -S $JOB_NAME npm start 32 | 33 | # keep only the most recent prev build to rollback if needed 34 | # (don't want to keep all and fill up the disk) 35 | rm -r $WORKSPACE-prev || echo "No prev build to delete" 36 | mv $WORKSPACE-`expr $BUILD_NUMBER - 1` $WORKSPACE-prev || echo "No prev build to be moved" 37 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "./src/model", 4 | "./src/server", 5 | "./src/util" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const { env } = require('process'); 2 | const isProd = env.NODE_ENV === 'production'; 3 | const isNonNil = x => x != null; 4 | 5 | let conf = { 6 | plugins: [ 7 | require('postcss-import')(), 8 | require('postcss-url')([ 9 | { 10 | filter: '**/*.svg', 11 | url: 'inline', 12 | encodeType: 'encodeURIComponent', 13 | optimizeSvgEncode: true, 14 | maxSize: 20 15 | }, 16 | { 17 | filter: '**/*.woff', 18 | url: 'inline', 19 | encodeType: 'base64', 20 | maxSize: Number.MAX_SAFE_INTEGER 21 | }, 22 | { 23 | filter: '**/*.woff2', 24 | url: 'inline', 25 | encodeType: 'base64', 26 | maxSize: Number.MAX_SAFE_INTEGER 27 | } 28 | ]), 29 | require('postcss-cssnext')({ 30 | browsers: require('./package.json').browserslist, 31 | warnForDuplicates: false 32 | }), 33 | isProd ? require('cssnano')({ 34 | safe: true 35 | }) : null 36 | ].filter( isNonNil ) 37 | }; 38 | 39 | module.exports = conf; 40 | -------------------------------------------------------------------------------- /private/res/banner-bottom-black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/private/res/banner-bottom-black.jpg -------------------------------------------------------------------------------- /private/res/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/private/res/banner.jpg -------------------------------------------------------------------------------- /private/res/icon_biofactoid_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /private/res/icon_biofactoid_filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /private/res/icon_biofactoid_outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /private/res/logos.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/private/res/logos.pdf -------------------------------------------------------------------------------- /private/res/shield_harvard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/private/res/shield_harvard.png -------------------------------------------------------------------------------- /private/res/shield_ohsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/private/res/shield_ohsu.png -------------------------------------------------------------------------------- /private/res/shield_uoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/private/res/shield_uoft.png -------------------------------------------------------------------------------- /public/bundle.css: -------------------------------------------------------------------------------- 1 | ../build/bundle.css -------------------------------------------------------------------------------- /public/bundle.js: -------------------------------------------------------------------------------- 1 | ../build/bundle.js -------------------------------------------------------------------------------- /public/deps.css: -------------------------------------------------------------------------------- 1 | ../build/deps.css -------------------------------------------------------------------------------- /public/deps.js: -------------------------------------------------------------------------------- 1 | ../build/deps.js -------------------------------------------------------------------------------- /public/image: -------------------------------------------------------------------------------- 1 | ../src/styles/image/ -------------------------------------------------------------------------------- /public/polyfills.js: -------------------------------------------------------------------------------- 1 | ../build/polyfills.js -------------------------------------------------------------------------------- /public/webpackjsonp.js: -------------------------------------------------------------------------------- 1 | ../build/webpackjsonp.js -------------------------------------------------------------------------------- /src/client-env-vars.json: -------------------------------------------------------------------------------- 1 | [ 2 | "NODE_ENV", 3 | "PC_URL", 4 | "BASE_URL", 5 | "LOG_LEVEL", 6 | "TWITTER_ACCOUNT_NAME", 7 | "MAX_TWEET_LENGTH", 8 | "DEMO_ID", 9 | "DEMO_SECRET", 10 | "DEMO_AUTHOR_EMAIL", 11 | "UNIPROT_LINK_BASE_URL", 12 | "PUBMED_LINK_BASE_URL", 13 | "PUBCHEM_LINK_BASE_URL", 14 | "NCBI_LINK_BASE_URL", 15 | "IDENTIFIERS_ORG_ID_BASE_URL", 16 | "DOI_LINK_BASE_URL", 17 | "GOOGLE_SCHOLAR_BASE_URL", 18 | "CHEBI_LINK_BASE_URL", 19 | "EMAIL_TYPE_FOLLOWUP", 20 | "EMAIL_TYPE_INVITE", 21 | "DEMO_CAN_BE_SHARED", 22 | "DEMO_CAN_BE_SHARED_MULTIPLE_TIMES", 23 | "SAMPLE_DOC_ID", 24 | "EMAIL_ADDRESS_INFO", 25 | "MAX_WAIT_TWEET", 26 | "ORCID_BASE_URL" 27 | ] -------------------------------------------------------------------------------- /src/client/components/accordion.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | import { Component } from 'react'; 3 | import { makeClassList } from '../dom'; 4 | import _ from 'lodash'; 5 | 6 | 7 | export const ACCORDION_ITEM_FIELDS = { 8 | TITLE: 'title', 9 | DESCRIPTION: 'description' 10 | }; 11 | 12 | 13 | export class AccordionItem extends Component { 14 | constructor(props){ 15 | super(props); 16 | 17 | this.state = { 18 | isOpen: false 19 | }; 20 | } 21 | 22 | toggleItem(){ 23 | this.setState({ isOpen: !this.state.isOpen }); 24 | } 25 | 26 | render(){ 27 | const { title, description } = this.props.item; 28 | const { isOpen } = this.state; 29 | const content = _.isString( description ) ? [ h('p', description) ] : description; 30 | return h('div.accordion-item', [ 31 | h('div.accordion-item-header', { 32 | className: makeClassList({ 33 | 'open': isOpen 34 | }), 35 | onClick: () => this.toggleItem() 36 | }, [ 37 | h( 'p.accordion-item-header-title', title ), 38 | isOpen ? h('i.material-icons.accordion-item-header-icon', 'expand_less') : 39 | h('i.material-icons.accordion-item-header-icon', 'expand_more') 40 | ]), 41 | h('div.accordion-item-content', content ) 42 | ]); 43 | } 44 | } 45 | 46 | export class Accordion extends Component { 47 | constructor(props){ 48 | super(props); 49 | 50 | this.state = { 51 | }; 52 | } 53 | 54 | render(){ 55 | const { title, items } = this.props; 56 | return h('div.accordion', [ 57 | title ? h('h3.accordion-title', title ) : null, 58 | h('div.accordion-items', items.map( (item, key) => h( AccordionItem, { key, item } ) ) ) 59 | ]); 60 | } 61 | } 62 | 63 | export default Accordion; -------------------------------------------------------------------------------- /src/client/components/animate.js: -------------------------------------------------------------------------------- 1 | import * as defs from '../defs'; 2 | import anime from 'animejs'; 3 | 4 | const animateDomForEdit = domEle => anime({ 5 | targets: domEle, 6 | backgroundColor: [defs.editAnimationWhite, defs.editAnimationColor, defs.editAnimationWhite], 7 | duration: defs.editAnimationDuration, 8 | easing: defs.editAnimationEasing 9 | }); 10 | 11 | export { animateDomForEdit }; 12 | -------------------------------------------------------------------------------- /src/client/components/copy-field.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import h from 'react-hyperscript'; 3 | import Tooltip from './popover/tooltip'; 4 | import ReactDom from 'react-dom'; 5 | import Clipboard from 'clipboard'; 6 | 7 | class CopyField extends Component { 8 | constructor( props ){ 9 | super( props ); 10 | 11 | this.state = { 12 | copied: false 13 | }; 14 | } 15 | 16 | render(){ 17 | let { copied } = this.state; 18 | let { value } = this.props; 19 | 20 | return h('div.copy-field', [ 21 | h('input.copy-field-input.input-joined.code', { type: 'text', value, readOnly: true }), 22 | h(Tooltip, { 23 | description: copied ? 'Copied' : 'Copy', 24 | tippy: { 25 | hideOnClick: false, 26 | trigger: 'mouseenter', 27 | position: 'left', 28 | sticky: true 29 | } 30 | }, [ 31 | h('button.button-joined.copy-field-copy', [ 32 | h('i.material-icons', 'content_paste') 33 | ]) 34 | ]) 35 | ]); 36 | } 37 | 38 | componentDidMount(){ 39 | let self = this; 40 | let root = ReactDom.findDOMNode( self ); 41 | let text = root.querySelector('input'); 42 | let btn = root.querySelector('button'); 43 | 44 | text.addEventListener('click', () => text.select()); 45 | 46 | let cp = new Clipboard(btn, { 47 | text: () => text.value 48 | }); 49 | 50 | cp.on('success', () => { 51 | self.setState({ copied: true }); 52 | }); 53 | 54 | btn.addEventListener('mouseleave', () => { 55 | self.setState({ copied: false }); 56 | }); 57 | } 58 | } 59 | 60 | export default CopyField; 61 | -------------------------------------------------------------------------------- /src/client/components/data-component.js: -------------------------------------------------------------------------------- 1 | import DirtyComponent from './dirty-component'; 2 | import _ from 'lodash'; 3 | 4 | class DataComponent extends DirtyComponent { 5 | constructor(props){ 6 | super(props); 7 | 8 | this.data = {}; 9 | } 10 | 11 | setData( name, value, callback ){ 12 | if( _.isObject(name) ){ 13 | callback = value; 14 | 15 | _.assign( this.data, name ); 16 | } else { 17 | this.data[ name ] = value; 18 | } 19 | 20 | this.dirty( callback ); 21 | } 22 | } 23 | 24 | export default DataComponent; 25 | -------------------------------------------------------------------------------- /src/client/components/dirty-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class DirtyComponent extends React.Component { 4 | constructor( props ){ 5 | super( props ); 6 | 7 | this.state = { _dirtyTimestamp: 0 }; 8 | } 9 | 10 | dirty( callback ){ 11 | this.setState({ _dirtyTimestamp: Date.now() }, callback); 12 | } 13 | 14 | clean(){} 15 | 16 | render( content ){ 17 | return content; 18 | } 19 | } 20 | 21 | export default DirtyComponent; 22 | -------------------------------------------------------------------------------- /src/client/components/document-linkout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import h from 'react-hyperscript'; 3 | import CopyField from './copy-field'; 4 | 5 | class Address extends React.Component { 6 | constructor( props ){ 7 | super( props ); 8 | } 9 | 10 | render(){ 11 | let { name, url, descr } = this.props; 12 | let formedUrl = location.protocol + '//' + location.host + url; 13 | 14 | return h('div.document-linkout-address', [ 15 | h('h3.document-linkout-address-name', name), 16 | h('p.document-linkout-address-descr', descr), 17 | h('div.document-linkout-address-val', [ 18 | h(CopyField, { value: formedUrl }) 19 | ]) 20 | ]); 21 | } 22 | } 23 | 24 | class Linkout extends React.Component { 25 | constructor( props ){ 26 | super( props ); 27 | } 28 | 29 | render(){ 30 | let doc = this.props.document; 31 | let docJson = this.props.documentJson; 32 | 33 | if( doc != null ){ 34 | docJson = { 35 | publicUrl: doc.publicUrl(), 36 | privateUrl: doc.privateUrl(), 37 | editable: doc.editable() 38 | }; 39 | } 40 | 41 | return h('div.document-linkout', [ 42 | h(Address, { 43 | name: 'Public address', 44 | url: docJson.publicUrl, 45 | descr: 'This is a read-only link for this document. Share this link with anyone.' 46 | }) 47 | ].concat( docJson.editable ? [ 48 | h(Address, { 49 | name: 'Private address', 50 | url: docJson.privateUrl, 51 | descr: 'This is a read-and-write link for this document. Share this link only with fellow editors.' 52 | }) 53 | ] : [] )); 54 | } 55 | } 56 | 57 | export default Linkout; 58 | -------------------------------------------------------------------------------- /src/client/components/editor/caption.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | import { Component } from 'react'; 3 | // import _ from 'lodash'; 4 | import TextareaAutosize from 'react-autosize-textarea'; 5 | import { makeClassList } from '../../dom'; 6 | 7 | export class Caption extends Component { 8 | constructor(props){ 9 | super(props); 10 | 11 | this.state = ({ caption: props.document.caption() }); 12 | } 13 | 14 | render(){ 15 | const { document } = this.props; 16 | 17 | return h('div.editor-caption', [ 18 | document.editable() ? 19 | h(TextareaAutosize, { 20 | className: makeClassList({ 21 | 'editor-caption-textarea': true 22 | }), 23 | value: this.state.caption, 24 | placeholder: `List terms that describe the context (e.g. T cell, cancer, genome stability)`, 25 | onChange: event => { 26 | const val = event.target.value; 27 | 28 | this.setState({ caption: val }); 29 | 30 | document.caption(val); 31 | } 32 | }) : 33 | h('div.editor-caption-text', document.caption()) 34 | ]); 35 | } 36 | } 37 | 38 | export default Caption; -------------------------------------------------------------------------------- /src/client/components/editor/cy/defs.js: -------------------------------------------------------------------------------- 1 | import * as clientDefs from '../../../defs'; 2 | 3 | export const padding = 20; 4 | export const minZoom = 1.2; 5 | export const maxZoom = 2.75; 6 | export const layoutAnimationDuration = clientDefs.updateDelay * 2/3; 7 | export const layoutAnimationEasing = 'ease-in-out-quint'; // spring(500, 35) 8 | export const positionDebounceTime = clientDefs.updateDelay; 9 | export const docPositionDebounceTime = 0; 10 | export const positionAnimationDuration = clientDefs.updateDelay / 2; 11 | export const positionAnimationEasing = 'ease-in-out-quint'; // spring(500, 35) 12 | export const addRmAnimationDuration = clientDefs.updateDelay / 3; 13 | export const addRmAnimationEasing = 'linear'; 14 | export const editAnimationDuration = clientDefs.editAnimationDuration; 15 | export const editAnimationEasing = 'linear'; 16 | export const editAnimationColor = 'rgb(255, 255, 0)'; 17 | export const editAnimationOpacity = 0.5; 18 | -------------------------------------------------------------------------------- /src/client/components/editor/cy/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Cytoscape from 'cytoscape'; 3 | import makeStylesheet from './stylesheet'; 4 | import handleLayout from './layout'; 5 | import handleViewport from './viewport'; 6 | import addEdgehandles from './edgehandles'; 7 | import addTippy from './tippy'; 8 | import addCompoundDnd from './compound-dnd'; 9 | import handleDoc from './doc'; 10 | import debug from '../../../debug'; 11 | import * as defs from './defs'; 12 | 13 | function makeCytoscape( opts ){ 14 | const editable = opts.document.editable(); 15 | 16 | const cy = new Cytoscape({ 17 | container: opts.container, 18 | style: makeStylesheet( opts.document ), 19 | minZoom: editable? defs.minZoom : 0.01, 20 | maxZoom: defs.maxZoom, 21 | zoom: ( defs.minZoom + defs.maxZoom ) / 2, 22 | userZoomingEnabled: editable, 23 | userPanningEnabled: editable, 24 | boxSelectionEnabled: editable, 25 | elements: [], 26 | layout: { 27 | name: 'preset', 28 | fit: false 29 | } 30 | }); 31 | 32 | if( debug.enabled() ){ 33 | window.cy = cy; 34 | } 35 | 36 | let handleOpts = _.assign( { cy }, opts ); 37 | 38 | [ 39 | handleLayout, 40 | handleViewport, 41 | addEdgehandles, 42 | addTippy, 43 | handleDoc, 44 | addCompoundDnd 45 | ].forEach( fn => fn( handleOpts ) ); 46 | 47 | return cy; 48 | } 49 | 50 | export default makeCytoscape; 51 | -------------------------------------------------------------------------------- /src/client/components/editor/cy/layout.js: -------------------------------------------------------------------------------- 1 | // import on from './on-key'; 2 | import * as defs from './defs'; 3 | import _ from 'lodash'; 4 | import { isInteractionNode, getCyLayoutOpts } from '../../../../util'; 5 | 6 | let isInteraction = isInteractionNode; 7 | 8 | let isNotInteraction = n => !isInteraction( n ); 9 | 10 | export default function( { bus, cy, document } ){ 11 | let lastLayout; 12 | 13 | let layout = () => { 14 | if( !document.editable() ){ return; } 15 | 16 | let opts = _.assign( {}, getCyLayoutOpts(), { 17 | padding: defs.padding 18 | } ); 19 | 20 | if( lastLayout ){ 21 | lastLayout.stop(); 22 | } 23 | 24 | lastLayout = cy.layout( _.assign( {}, opts, { 25 | animate: 'end', 26 | animationDuration: defs.layoutAnimationDuration, 27 | animationEasing: defs.layoutAnimationEasing, 28 | animationFilter: isNotInteraction 29 | } ) ); 30 | 31 | lastLayout.run(); 32 | }; 33 | 34 | // on('r', layout); 35 | bus.on('layout', layout); 36 | } 37 | -------------------------------------------------------------------------------- /src/client/components/editor/cy/on-key.js: -------------------------------------------------------------------------------- 1 | import Mousetrap from 'mousetrap'; 2 | let on = Mousetrap.bind.bind( Mousetrap ); 3 | 4 | export default on; 5 | -------------------------------------------------------------------------------- /src/client/components/editor/cy/stylesheet.js: -------------------------------------------------------------------------------- 1 | import { makeStylesheet } from '../../../../util'; 2 | 3 | export default makeStylesheet; 4 | -------------------------------------------------------------------------------- /src/client/components/editor/cy/viewport.js: -------------------------------------------------------------------------------- 1 | import on from './on-key'; 2 | import * as defs from './defs'; 3 | 4 | function handlePan( cy ){ 5 | let panAmount = 5; 6 | let panMult = 4; 7 | 8 | on('down', () => cy.panBy({ y: -panAmount * panMult })); 9 | on('up', () => cy.panBy({ y: panAmount * panMult })); 10 | on('left', () => cy.panBy({ x: panAmount * panMult })); 11 | on('right', () => cy.panBy({ x: -panAmount * panMult })); 12 | on('shift+down', () => cy.panBy({ y: -panAmount })); 13 | on('shift+up', () => cy.panBy({ y: panAmount })); 14 | on('shift+left', () => cy.panBy({ x: panAmount })); 15 | on('shift+right', () => cy.panBy({ x: -panAmount })); 16 | } 17 | 18 | function handleZoom( cy ){ 19 | let zoomAmount = 0.05; 20 | let zoomMult = 4; 21 | 22 | let zoomBy = mult => { 23 | let z = cy.zoom(); 24 | let w = cy.width(); 25 | let h = cy.height(); 26 | 27 | cy.zoom({ 28 | level: z * mult, 29 | renderedPosition: { x: w/2, y: h/2 } 30 | }); 31 | }; 32 | 33 | on('=', () => zoomBy( (1 + zoomAmount * zoomMult) )); 34 | on('-', () => zoomBy( 1/(1 + zoomAmount * zoomMult) )); 35 | on('+', () => zoomBy( 1 + zoomAmount )); 36 | on('_', () => zoomBy( 1/(1 + zoomAmount) )); 37 | } 38 | 39 | function handleFit( cy, bus ){ 40 | let fit = () => { 41 | cy.fit(defs.padding); 42 | }; 43 | 44 | on('f', fit); 45 | bus.on('fit', fit); 46 | } 47 | 48 | export default function( { bus, cy } ){ 49 | handlePan( cy ); 50 | handleZoom( cy ); 51 | handleFit( cy, bus ); 52 | } 53 | -------------------------------------------------------------------------------- /src/client/components/editor/defs.js: -------------------------------------------------------------------------------- 1 | export const newElementPosition = { x: 110, y: 120 }; 2 | export const newElementShift = 10; 3 | export const newElementMaxShifts = 2; 4 | -------------------------------------------------------------------------------- /src/client/components/editor/explore-share.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import h from 'react-hyperscript'; 3 | import { NativeShare, isNativeShareSupported } from '../native-share'; 4 | import { BASE_URL } from '../../../config'; 5 | 6 | export class ExploreShare extends Component { 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | render(){ 12 | const { document } = this.props; 13 | 14 | if( document.editable() ){ 15 | return null; 16 | } 17 | 18 | const tweetUrl = document.tweetUrl(); 19 | 20 | return h('div.editor-explore-share', [ 21 | (tweetUrl ? h('a', { 22 | href: tweetUrl, 23 | target: '_blank' 24 | }, [ 25 | h('button.editor-explore-share-twitter.super-salient-button', [ 26 | h('i.icon.icon-t-white') 27 | ]), 28 | ]) : null), 29 | (isNativeShareSupported() ? h(NativeShare, { 30 | title: document.citation().title, 31 | text: '', 32 | url: BASE_URL + document.publicUrl(), 33 | buttonClass: 'super-salient-button' 34 | }, [ 35 | h('span', 'Share') 36 | ]) : null) 37 | ]); 38 | } 39 | } 40 | 41 | export default ExploreShare; -------------------------------------------------------------------------------- /src/client/components/editor/help.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | import { makeClassList } from '../../dom'; 3 | 4 | export const Help = props => { 5 | const { showHelp, controller, document } = props; 6 | 7 | if( !document.editable() ){ 8 | return null; 9 | } 10 | 11 | return h('div.editor-help-container', [ 12 | h('div.editor-help-background', { 13 | className: makeClassList({ 14 | 'editor-help-background-shown': showHelp 15 | }), 16 | onClick: () => controller.toggleHelp() 17 | }), 18 | h('div.editor-help', { 19 | className: makeClassList({ 20 | 'editor-help-shown': showHelp 21 | }) 22 | }, [ 23 | h('div.editor-help-box', [ 24 | h('div.editor-help-close-icon', { 25 | onClick: () => controller.toggleHelp() 26 | }, [ 27 | h('i.material-icons', 'close') 28 | ]), 29 | h('div.editor-help-title', 'Welcome'), 30 | h('div.editor-scroll-box', [ 31 | h('div.editor-help-copy', ` 32 | In just a few simple steps you'll compose a profile containing the key biological interactions described in your article. 33 | `), 34 | h('div.editor-help-cells', [ 35 | h('div.editor-help-cell', [ 36 | h('img.editor-help-img', { src: '/image/welcome-aboard-1.svg' }), 37 | h('div.editor-help-caption', `1. Add your genes and chemicals`) 38 | ]), 39 | h('div.editor-help-cell', [ 40 | h('img.editor-help-img', { src: '/image/welcome-aboard-2.svg' }), 41 | h('div.editor-help-caption', `2. Connect those that interact`) 42 | ]), 43 | h('div.editor-help-cell', [ 44 | h('img.editor-help-img', { src: '/image/welcome-aboard-3.svg' }), 45 | h('div.editor-help-caption', `3. For complexes, drag items together`) 46 | ]), 47 | h('div.editor-help-cell', [ 48 | h('img.editor-help-img', { src: '/image/welcome-aboard-4.svg' }), 49 | h('div.editor-help-caption', `4. Submit to finish`) 50 | ]) 51 | ]) 52 | ]), 53 | h('div.editor-help-close', [ 54 | h('button.editor-help-close-button.active-button', { 55 | onClick: () => controller.toggleHelp() 56 | }, `OK, let's start`) 57 | ]) 58 | ]) 59 | ]) 60 | ]); 61 | }; 62 | 63 | export default Help; -------------------------------------------------------------------------------- /src/client/components/editor/submit.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | import { TaskView } from '../tasks'; 4 | import Popover from '../popover/popover'; 5 | import { makeClassList } from '../../dom'; 6 | import h from 'react-hyperscript'; 7 | 8 | export const Submit = props => { 9 | 10 | const emitter = new EventEmitter(); 11 | const { document, bus, controller } = props; 12 | 13 | if( !document.editable() ){ 14 | return null; 15 | } 16 | 17 | return h('div.editor-submit', [ 18 | h(Popover, { 19 | hide: hideNow => bus.on('closesubmit', hideNow), 20 | tippy: { 21 | html: h(TaskView, { document, bus, controller, emitter } ), 22 | sticky: true 23 | } 24 | }, [ 25 | h('button.editor-submit-button', { 26 | disabled: document.trashed(), 27 | className: makeClassList({ 28 | 'super-salient-button': true, 29 | 'submitted': controller.done() 30 | }) 31 | }, controller.done() ? 'Submitted' : 'Submit') 32 | ]) 33 | ]); 34 | }; 35 | 36 | export default Submit; -------------------------------------------------------------------------------- /src/client/components/editor/title.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | import { Component } from 'react'; 3 | import _ from 'lodash'; 4 | 5 | import Popover from '../popover/popover'; 6 | import RequestForm from '../request-form'; 7 | import Citation from '../citation'; 8 | 9 | class EditorTitle extends Component { 10 | constructor(props){ 11 | super(props); 12 | 13 | const { bus, document } = this.props; 14 | const citation = document.citation(); 15 | 16 | this.bus = bus; 17 | 18 | this.state = { 19 | citation 20 | }; 21 | 22 | this.onRequestBtnClick = () => this.setState({ citation: document.citation() }); 23 | } 24 | 25 | componentDidMount(){ 26 | this.bus.on('requestBtnClick', this.onRequestBtnClick); 27 | } 28 | 29 | componentWillUnmount(){ 30 | this.bus.removeListener('requestBtnClick', this.onRequestBtnClick); 31 | } 32 | 33 | render(){ 34 | const { document } = this.props; 35 | if( !document.editable() ) return null; 36 | 37 | const { citation } = this.state; 38 | 39 | return h('div.editor-title', [ 40 | h('div.editor-title-content', [ 41 | citation.title ? h(Citation, { document, compact: true }) : 42 | h( Popover, { 43 | show: showTippy => { 44 | this.bus.on('showedittitle', showTippy); // for submit validation: show me how 45 | }, 46 | tippy: { 47 | html: h( RequestForm, { 48 | doc: document, 49 | bus: this.bus, 50 | submitBtnText: 'OK', 51 | showDescription: false, 52 | showTitle: false, 53 | addClasses: '.editor-request-form-container', 54 | formFields: { 55 | authorName: _.get(document.provided(), ['authorName']), 56 | authorEmail: _.get(document.provided(), ['authorEmail']) 57 | } 58 | }), 59 | onShown: () => this.bus.emit('opencta'), 60 | onHidden: () => this.bus.emit( 'closecta' ), 61 | placement: 'top' 62 | } 63 | }, [ 64 | h( 'div.editor-title-cta', [ 65 | h( 'span.plain-link.link-like', 'Set article information' ) 66 | ]) 67 | ]) 68 | ]) 69 | ]); 70 | } 71 | } 72 | 73 | export default EditorTitle; -------------------------------------------------------------------------------- /src/client/components/editor/undo-remove.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | 3 | export default function({ controller }){ 4 | let avail = controller.data.undoRemoveAvailable; 5 | 6 | return h('div.editor-undo-rm' + (avail ? '' : '.editor-undo-rm-unavailable'), [ 7 | h('button.plain-button.editor-undo-rm-button', { 8 | onClick: () => controller.undoRemove() 9 | }, [ 10 | h('i.material-icons', 'undo'), 11 | h('span', ' Undo last delete') 12 | ]) 13 | ]); 14 | } 15 | -------------------------------------------------------------------------------- /src/client/components/element-info/chemical-formula.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | 3 | const Formula = ({ formula }) => { 4 | let split = formula.match(/(\d+|[n]|[A-Z][a-z]?)/g); 5 | let children = []; 6 | 7 | split.forEach( str => { 8 | if( str === 'n' || isNaN( parseInt(str) ) ){ 9 | children.push( h('span', str) ); 10 | } else { 11 | children.push( h('sub', str) ); 12 | } 13 | } ); 14 | 15 | return h('span.entity-info-formula', children); 16 | }; 17 | 18 | export default Formula; 19 | -------------------------------------------------------------------------------- /src/client/components/element-info/element-info.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import h from 'react-hyperscript'; 3 | import EntityInfo from './entity-info'; 4 | import InteractionInfo from './interaction-info'; 5 | 6 | class ElementInfo extends React.Component { 7 | constructor( props ){ 8 | super( props ); 9 | } 10 | 11 | render(){ 12 | let p = Object.assign( { 13 | key: this.props.element.id() 14 | }, this.props ); 15 | 16 | let { element } = p; 17 | 18 | let component; 19 | 20 | if( element.isEntity() ){ 21 | component = h( EntityInfo, p ); 22 | } else if( element.isInteraction() ){ 23 | component = h( InteractionInfo, p ); 24 | } 25 | 26 | return ( 27 | h('div.element-info', [ component ] ) 28 | ); 29 | } 30 | } 31 | 32 | export default ElementInfo; 33 | -------------------------------------------------------------------------------- /src/client/components/element-info/progression-stepper.js: -------------------------------------------------------------------------------- 1 | import Tooltip from '../popover/tooltip'; 2 | import h from 'react-hyperscript'; 3 | import { makeClassList } from '../../dom'; 4 | 5 | export default ({ progression }) => { 6 | let { STAGES } = progression; 7 | let stage = progression.getStage(); 8 | let isCompleted = stage === STAGES.COMPLETED; 9 | let buttonLabel = content => h('span.element-info-progression-button-label', [ content ]); 10 | 11 | let backButtonLabel = buttonLabel( h('i.material-icons', 'chevron_left') ); 12 | 13 | let forwardButtonLabel = buttonLabel( 14 | h('i.material-icons', { 15 | className: makeClassList({ 16 | 'element-info-complete-icon': isCompleted 17 | }) 18 | }, isCompleted ? 'check_circle' : 'chevron_right') 19 | ); 20 | 21 | let tippyOpts = { placement: 'bottom' }; 22 | 23 | return ( h('div.element-info-progression', [ 24 | h('div.element-info-back-area', [ 25 | h(Tooltip, { 26 | description: 'Back', 27 | tippy: tippyOpts 28 | }, [ 29 | h('button.element-info-back.plain-button', { 30 | disabled: !progression.canGoBack(), 31 | onClick: () => progression.back() 32 | }, [ 33 | backButtonLabel 34 | ]) 35 | ]) 36 | ]), 37 | 38 | h(Tooltip, { 39 | description: isCompleted ? 'Completed' : 'Next', 40 | tippy: tippyOpts 41 | }, [ 42 | h('div.element-info-forward-area', [ 43 | h('button.element-info-forward.plain-button', { 44 | disabled: !progression.canGoForward(), 45 | onClick: () => progression.forward() 46 | }, [ 47 | forwardButtonLabel 48 | ]) 49 | ]) 50 | ]) 51 | ]) ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/client/components/element-info/progression.js: -------------------------------------------------------------------------------- 1 | import { error } from '../../../util'; 2 | 3 | class Progression { 4 | constructor({ STAGES, canGoToStage, getStage, goToStage }){ 5 | this.STAGES = {}; 6 | 7 | STAGES.forEach( key => { 8 | this.STAGES[key] = key; 9 | } ); 10 | 11 | if( STAGES[STAGES.length-1] !== 'COMPLETED' ){ 12 | throw error('A Progression must have COMPLETED as the final stage'); 13 | } 14 | 15 | this.ORDERED_STAGES = STAGES.slice(); 16 | 17 | this.getStage = getStage; 18 | 19 | this.canGoToStage = canGoToStage; 20 | 21 | this.goToStage = goToStage; 22 | } 23 | 24 | getStageIndex( stage ){ 25 | return this.ORDERED_STAGES.indexOf( stage ); 26 | } 27 | 28 | getNextStage( stage ){ 29 | return this.ORDERED_STAGES[ this.getStageIndex(stage) + 1 ]; 30 | } 31 | 32 | getPrevStage( stage ){ 33 | return this.ORDERED_STAGES[ this.getStageIndex(stage) - 1 ]; 34 | } 35 | 36 | back(){ 37 | this.goToStage( this.getPrevStage( this.getStage() ) ); 38 | } 39 | 40 | forward(){ 41 | this.goToStage( this.getNextStage( this.getStage() ) ); 42 | } 43 | 44 | canGoBack(){ 45 | return this.canGoToStage( this.getPrevStage( this.getStage() ) ); 46 | } 47 | 48 | canGoForward(){ 49 | return this.canGoToStage( this.getNextStage( this.getStage() ) ); 50 | } 51 | } 52 | 53 | export default Progression; 54 | -------------------------------------------------------------------------------- /src/client/components/native-share.js: -------------------------------------------------------------------------------- 1 | import DataComponent from './data-component'; 2 | import h from 'react-hyperscript'; 3 | import _ from 'lodash'; 4 | 5 | export class NativeShare extends DataComponent { 6 | constructor(props){ 7 | super(props); 8 | } 9 | 10 | share(){ 11 | const { title, url, text } = this.props; 12 | 13 | navigator.share({ title, url, text }); 14 | } 15 | 16 | render(){ 17 | const children = this.props.children || [ 18 | h('i.icon.icon-shr.native-share-icon') 19 | ]; 20 | 21 | const buttonClass= this.props.buttonClass || 'plain-button'; 22 | 23 | return h('span.native-share', [ 24 | h('button.native-shr-button.' + buttonClass, { 25 | onClick: () => this.share() 26 | }, children) 27 | ]); 28 | } 29 | } 30 | 31 | export function isNativeShareSupported(){ 32 | return 'share' in navigator && _.isFunction(navigator.share); 33 | } 34 | 35 | export default NativeShare; 36 | -------------------------------------------------------------------------------- /src/client/components/notification/base.js: -------------------------------------------------------------------------------- 1 | import DirtyComponent from '../dirty-component'; 2 | import h from 'react-hyperscript'; 3 | import { makeClassList } from '../../dom'; 4 | 5 | /** 6 | * A base component for notifications. A concrete component should contain a NotifcationBase. 7 | */ 8 | class NotificationBase extends DirtyComponent { 9 | constructor( props ){ 10 | super( props ); 11 | } 12 | 13 | componentDidMount(){ 14 | let { notification: ntfn } = this.props; 15 | 16 | this.onChange = () => this.dirty(); 17 | 18 | ntfn.on('change', this.onChange); 19 | } 20 | 21 | componentWillUnmount(){ 22 | let { notification: ntfn } = this.props; 23 | 24 | ntfn.removeListener('change', this.onChange); 25 | } 26 | 27 | render(){ 28 | let p = this.props; 29 | let n = p.notification; 30 | 31 | return ( h('div.notification', { 32 | className: makeClassList({ 33 | 'notification-active': n.active(), 34 | 'notification-inactive': !n.active(), 35 | 'notification-dismissed': n.dismissed(), 36 | 'notification-openable': n.openable() 37 | }) + ( p.className ? ' ' + p.className : '' ) 38 | }, [ 39 | h('div.notification-content', { 40 | onClick: () => { 41 | if( n.openable() ){ n.open(); } 42 | } 43 | }, [ 44 | n.title() ? h('div.notification-title', n.title()) : null, 45 | h('div.notification-message', n.message()) 46 | ].filter( v => v != null )), 47 | h('div.notification-actions', [ 48 | n.options.dismissable ? h('button.notification-action.notification-dismiss', { onClick: () => n.dismiss() }, [ 49 | 'Dismiss' 50 | ]) : null, 51 | h('button.notification-action.notification-open', { onClick: () => n.open() }, n.openText()) 52 | ]) 53 | ]) ); 54 | } 55 | } 56 | 57 | export default props => h(NotificationBase, Object.assign({ key: props.notification.id() }, props)); 58 | -------------------------------------------------------------------------------- /src/client/components/notification/corner.js: -------------------------------------------------------------------------------- 1 | import DirtyComponent from '../dirty-component'; 2 | import h from 'react-hyperscript'; 3 | import NotificationBase from './base'; 4 | import { makeClassList } from '../../dom'; 5 | 6 | class CornerNotification extends DirtyComponent { 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | componentDidMount(){ 12 | let { notification: ntfn } = this.props; 13 | 14 | this.onActivationChange = () => this.dirty(); 15 | 16 | ntfn.on('activate', this.onActivationChange); 17 | ntfn.on('deactivate', this.onActivationChange); 18 | } 19 | 20 | componentWillUnmount(){ 21 | let { notification: ntfn } = this.props; 22 | 23 | ntfn.removeListener('activate', this.onActivationChange); 24 | ntfn.removeListener('deactivate', this.onActivationChange); 25 | } 26 | 27 | render(){ 28 | let { notification, className } = this.props; 29 | 30 | return ( h('div.corner-notification', { 31 | className: makeClassList({ 32 | 'corner-notification-active': notification.active() 33 | }) + ' ' + className 34 | }, [ 35 | h(NotificationBase, { notification }) 36 | ]) ); 37 | } 38 | } 39 | 40 | export default props => h(CornerNotification, Object.assign({ key: props.notification.id() }, props)); 41 | -------------------------------------------------------------------------------- /src/client/components/notification/index.js: -------------------------------------------------------------------------------- 1 | // export the view object by default 2 | export { default } from './notification'; 3 | -------------------------------------------------------------------------------- /src/client/components/notification/inline.js: -------------------------------------------------------------------------------- 1 | import DirtyComponent from '../dirty-component'; 2 | import h from 'react-hyperscript'; 3 | import NotificationBase from './base'; 4 | import { makeClassList } from '../../dom'; 5 | 6 | class InlineNotification extends DirtyComponent { 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | componentDidMount(){ 12 | let { notification: ntfn } = this.props; 13 | 14 | this.onActivationChange = () => this.dirty(); 15 | 16 | ntfn.on('activate', this.onActivationChange); 17 | ntfn.on('deactivate', this.onActivationChange); 18 | } 19 | 20 | componentWillUnmount(){ 21 | let { notification: ntfn } = this.props; 22 | 23 | ntfn.removeListener('activate', this.onActivationChange); 24 | ntfn.removeListener('deactivate', this.onActivationChange); 25 | } 26 | 27 | render(){ 28 | let { notification, className } = this.props; 29 | 30 | return ( h('div.inline-notification', { 31 | className: makeClassList({ 32 | 'inline-notification-active': notification.active() 33 | }) + ' ' + className 34 | }, [ 35 | h(NotificationBase, { notification }) 36 | ]) ); 37 | } 38 | } 39 | 40 | export default props => h(InlineNotification, Object.assign({ key: props.notification.id() }, props)); 41 | -------------------------------------------------------------------------------- /src/client/components/notification/list.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import uuid from 'uuid'; 3 | 4 | import EventEmitterMixin from '../../../model/event-emitter-mixin'; 5 | import { mixin } from '../../../util'; 6 | 7 | class NotificationList { 8 | constructor( notifications = [], opts ){ 9 | EventEmitterMixin.call( this ); 10 | 11 | this.set = new Set( notifications ); 12 | 13 | this.options = _.assign({ 14 | id: uuid() 15 | }, opts); 16 | } 17 | 18 | id(){ 19 | return this.options.id; 20 | } 21 | 22 | add( ntfn ){ 23 | this.set.add( ntfn ); 24 | 25 | ntfn.on('dismiss', () => { 26 | this.delete( ntfn ); 27 | }); 28 | 29 | this.emit('add', ntfn); 30 | this.emit('change'); 31 | 32 | return this; 33 | } 34 | 35 | delete( ntfn ){ 36 | this.set.delete( ntfn ); 37 | 38 | this.emit('delete', ntfn); 39 | this.emit('change'); 40 | 41 | return this; 42 | } 43 | 44 | clear(){ 45 | this.set.clear(); 46 | 47 | this.emit('clear'); 48 | this.emit('change'); 49 | 50 | return this; 51 | } 52 | 53 | values(){ 54 | return this.set.values(); 55 | } 56 | 57 | forEach( fn, thisArg ){ 58 | return this.set.forEach( fn, thisArg ); 59 | } 60 | 61 | empty(){ 62 | return this.set.size === 0; 63 | } 64 | } 65 | 66 | NotificationList.prototype[ Symbol.iterator ] = NotificationList.prototype.values; 67 | 68 | mixin( NotificationList.prototype, EventEmitterMixin.prototype ); 69 | 70 | export default NotificationList; 71 | -------------------------------------------------------------------------------- /src/client/components/notification/notification.js: -------------------------------------------------------------------------------- 1 | import EventEmitterMixin from '../../../model/event-emitter-mixin'; 2 | import { mixin, error } from '../../../util'; 3 | import uuid from 'uuid'; 4 | 5 | const defaults = { 6 | title: '', 7 | message: '', 8 | openText: 'Open', 9 | active: false, 10 | openable: false, 11 | dismissable: true 12 | }; 13 | 14 | const setOrGet = ( ntfn, name, value, eventName = name ) => { 15 | if( value !== undefined ){ 16 | ntfn.options[name] = value; 17 | 18 | ntfn.emit( eventName ); 19 | ntfn.emit('change'); 20 | 21 | return this; 22 | } else { 23 | return ntfn.options[name]; 24 | } 25 | }; 26 | 27 | class Notification { 28 | constructor( options ){ 29 | EventEmitterMixin.call( this ); 30 | 31 | this.options = Object.assign( { 32 | id: uuid() 33 | }, defaults, options ); 34 | } 35 | 36 | id(){ 37 | return this.options.id; 38 | } 39 | 40 | title( newTitle ){ 41 | return setOrGet( this, 'title', newTitle ); 42 | } 43 | 44 | message( newMessage ){ 45 | return setOrGet( this, 'message', newMessage ); 46 | } 47 | 48 | // whether the notification is active/shown in the ui 49 | active(){ 50 | return this.options.active; 51 | } 52 | 53 | activate(){ 54 | return setOrGet( this, 'active', true, 'activate' ); 55 | } 56 | 57 | deactivate(){ 58 | return setOrGet( this, 'active', false, 'deactivate' ); 59 | } 60 | 61 | openable( newIsOpenable ){ 62 | return setOrGet( this, 'openable', newIsOpenable ); 63 | } 64 | 65 | // text for open action button 66 | openText( newText ){ 67 | return setOrGet( this, 'openText', newText ); 68 | } 69 | 70 | open(){ 71 | if( this.openable() ){ 72 | this.options.open = true; 73 | 74 | this.emit('open'); 75 | 76 | return this; 77 | } else { 78 | throw error('Non-openable notifcation can not be opened'); 79 | } 80 | } 81 | 82 | dismissed(){ 83 | return !this.options.active; 84 | } 85 | 86 | dismiss(){ 87 | return this.deactivate(); 88 | } 89 | } 90 | 91 | mixin( Notification.prototype, EventEmitterMixin.prototype ); 92 | 93 | export default Notification; 94 | -------------------------------------------------------------------------------- /src/client/components/notification/panel.js: -------------------------------------------------------------------------------- 1 | import DataComponent from '../data-component'; 2 | import h from 'react-hyperscript'; 3 | 4 | import InlineNotification from './inline'; 5 | import { makeClassList } from '../../dom'; 6 | 7 | // WIP 8 | class NotificationPanel extends DataComponent { 9 | constructor( props ){ 10 | super( props ); 11 | 12 | this.data = { open: false || props.open }; 13 | } 14 | 15 | componentDidMount(){ 16 | let { notificationList: nl } = this.props; 17 | 18 | this.onChange = () => this.dirty(); 19 | 20 | nl.on('change', this.onChange); 21 | } 22 | 23 | componentWillUnmount(){ 24 | let { notificationList: nl } = this.props; 25 | 26 | nl.removeListener('change', this.onChange); 27 | } 28 | 29 | open(){ 30 | this.setData({ open: true }); 31 | } 32 | 33 | close(){ 34 | this.setData({ open: false }); 35 | } 36 | 37 | render(){ 38 | let { notificationList: nl } = this.props; 39 | let { open } = this.data; 40 | 41 | let makeNtfn = notification => h('div.notification-panel-entry', [ 42 | h(InlineNotification, { notification, key: notification.id() }) 43 | ]); 44 | 45 | return ( 46 | h('div.notification-panel', { 47 | className: makeClassList({ 'notification-panel-open': open }) 48 | }, [ 49 | h('div.notification-panel-entries', Array.from( nl ).map( makeNtfn )) 50 | ]) 51 | ); 52 | } 53 | } 54 | 55 | export default NotificationPanel; 56 | -------------------------------------------------------------------------------- /src/client/components/notification/popover.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import NotificationBase from './base'; 3 | import Popover from '../popover'; 4 | import _ from 'lodash'; 5 | import { tippyDefaults } from '../../defs'; 6 | import h from 'react-hyperscript'; 7 | 8 | class PopoverNotification extends Component { 9 | constructor( props ){ 10 | super( props ); 11 | 12 | let { notification, tippy } = this.props; 13 | 14 | let tippyOptions = _.assign( {}, tippyDefaults, { 15 | html: (() => { 16 | return h('div.popover-notification-content', [ 17 | h(NotificationBase, { notification }) 18 | ]); 19 | })(), 20 | placement: 'top', 21 | trigger: 'manual', 22 | hideOnClick: false, 23 | interactive: true, 24 | touchHold: false, 25 | theme: 'dark', 26 | delay: 0 27 | }, tippy ); 28 | 29 | let popoverOptions = _.assign( {}, this.props, { 30 | tippy: tippyOptions, 31 | show: showNow => { 32 | let show = this.show = () => { 33 | if( this.data.mounted ){ 34 | showNow(); 35 | } 36 | }; 37 | 38 | notification.on('activate', show); 39 | 40 | if( notification.active() ){ 41 | show(); 42 | } 43 | }, 44 | hide: hideNow => { 45 | let hide = this.hide = () => { 46 | if( this.data.mounted ){ 47 | hideNow(); 48 | } 49 | }; 50 | 51 | notification.on('deactivate', hide); 52 | notification.on('dismiss', hide); 53 | } 54 | } ); 55 | 56 | this.popoverOptions = popoverOptions; 57 | } 58 | 59 | componentDidMount(){ 60 | this.data.mounted = true; 61 | } 62 | 63 | componentWillUnmount(){ 64 | let { notification } = this.data; 65 | let { show, hide } = this; 66 | 67 | this.data.mounted = false; 68 | 69 | notification.removeListener('activate', show); 70 | notification.removeListener('deactivate', hide); 71 | notification.removeListener('dismiss', hide); 72 | } 73 | 74 | render(){ 75 | return h( Popover, this.popoverOptions, this.props.children ); 76 | } 77 | } 78 | 79 | export default PopoverNotification; 80 | -------------------------------------------------------------------------------- /src/client/components/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import h from 'react-hyperscript'; 3 | 4 | class PageNotFound extends Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | render(){ 10 | return h('span', 'Page not found'); // TODO 11 | } 12 | } 13 | 14 | export default PageNotFound; 15 | -------------------------------------------------------------------------------- /src/client/components/popover/tooltip.js: -------------------------------------------------------------------------------- 1 | import Popover from './popover'; 2 | import React from 'react'; 3 | import _ from 'lodash'; 4 | import { tippyDefaults } from '../../defs'; 5 | import h from 'react-hyperscript'; 6 | 7 | class Tooltip extends React.Component { 8 | constructor( props ){ 9 | super( props ); 10 | } 11 | 12 | render(){ 13 | let props = this.props; 14 | 15 | let tippyOptions = _.assign( {}, tippyDefaults, { 16 | html: (() => { 17 | return h('div.tooltip-content', [ 18 | h('span.tooltip-description', props.description) 19 | ].concat(props.shortcut != null ? [ 20 | h('span.tooltip-shortcut-label', 'Keyboard'), 21 | h('span.tooltip-shortcut', [ 22 | h('code', props.shortcut) 23 | ]) 24 | ] : [])); 25 | })(), 26 | placement: 'right', 27 | trigger: 'mouseenter', 28 | hideOnClick: true, 29 | interactive: false, 30 | touchHold: true, 31 | theme: 'dark', 32 | delay: [ 1000, 0 ] 33 | }, props.tippy ); 34 | 35 | let popoverOptions = _.assign( {}, props, { tippy: tippyOptions } ); 36 | 37 | return h( Popover, popoverOptions, props.children ); 38 | } 39 | } 40 | 41 | module.exports = Tooltip; 42 | -------------------------------------------------------------------------------- /src/client/components/related-papers.js: -------------------------------------------------------------------------------- 1 | import h from 'react-hyperscript'; 2 | import { Component } from 'react'; 3 | import { DOI_LINK_BASE_URL, PUBMED_LINK_BASE_URL } from '../../config'; 4 | 5 | const MAX_PAPERS = 6; 6 | 7 | export class RelatedPapers extends Component { 8 | constructor(props){ 9 | super(props); 10 | 11 | this.state = { 12 | papers: this.props.source.relatedPapers() 13 | }; 14 | } 15 | 16 | componentDidMount(){ 17 | this.onRefresh = () => { 18 | this.setState({ papers: this.props.source.relatedPapers() }); 19 | }; 20 | 21 | this.props.source.on('relatedpapers', this.onRefresh); 22 | } 23 | 24 | componentWillUnmount(){ 25 | this.props.source.removeListener('relatedpapers', this.onRefresh); 26 | } 27 | 28 | render(){ 29 | let { papers } = this.state; 30 | 31 | if( !papers ){ 32 | return h('div.related-papers.related-papers-empty', [ 33 | h('div.related-papers-empty-icon', [ 34 | h('i.icon.icon-spinner') 35 | ]), 36 | h('p.related-papers-empty-msg', [ 37 | `Biofactoid is looking for other interesting articles.`, 38 | h('br'), 39 | `They'll be ready for you in a moment.` 40 | ]) 41 | ]); 42 | } 43 | 44 | papers = papers.slice(0, MAX_PAPERS); 45 | 46 | return h('div.related-papers', papers.map( paper => { 47 | const { pubmed: { title, authors: { abbreviation: author }, reference: journal, abstract, doi, pmid } } = paper; 48 | let link = `${DOI_LINK_BASE_URL}${doi}`; 49 | if( !doi ) link = `${PUBMED_LINK_BASE_URL}${pmid}`; 50 | 51 | return h('a.related-paper', { 52 | href: link, 53 | target: '_blank' 54 | }, [ 55 | h('div.related-paper-title', [ 56 | h('span.link-like.plain-link', title) 57 | ]), 58 | h('div.related-paper-abstract', abstract), 59 | h('div.related-paper-author', author), 60 | h('div.related-paper-journal', journal) 61 | ]); 62 | })); 63 | } 64 | } 65 | 66 | export default RelatedPapers; -------------------------------------------------------------------------------- /src/client/components/toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import h from 'react-hyperscript'; 3 | 4 | class Toggle extends React.Component { 5 | constructor( props ){ 6 | super( props ); 7 | } 8 | 9 | render(){ 10 | let p = this.props; 11 | let update = () => this.forceUpdate(); 12 | let on = p.getState(); 13 | 14 | return h('button', { 15 | onClick: evt => { 16 | let ret = p.onToggle( evt ); 17 | 18 | update(); // always update optimistically 19 | 20 | // if we have a promise, also update when it resolves 21 | if( ret != null && ret.then != null ){ 22 | ret.then( update ); 23 | } 24 | }, 25 | className: [ 26 | 'button-toggle', 27 | on ? 'button-toggle-on' : '' 28 | ].join(' ') + ' ' + p.className 29 | }, p.children); 30 | } 31 | } 32 | 33 | export default Toggle; 34 | -------------------------------------------------------------------------------- /src/client/debug.js: -------------------------------------------------------------------------------- 1 | import domReady from 'fready'; 2 | import logger from './logger'; 3 | 4 | let debug = window.dbg = { 5 | enabled: function( on ){ 6 | if( arguments.length === 0 ){ 7 | if( this._enabled != null ){ 8 | return this._enabled; 9 | } else { 10 | return window.DEBUG || process.env.NODE_ENV !== 'production'; 11 | } 12 | } else { 13 | this._enabled = !!on; 14 | } 15 | }, 16 | 17 | livereload: function(){ 18 | let script = document.createElement('script'); 19 | script.src = 'http://' + window.location.hostname + ':35729/livereload.js'; 20 | 21 | document.head.prepend( script ); 22 | }, 23 | 24 | init: function(){ 25 | domReady( () => this.livereload() ); 26 | }, 27 | 28 | logger: function(){ 29 | return logger; 30 | } 31 | }; 32 | 33 | export default debug; 34 | -------------------------------------------------------------------------------- /src/client/defs.js: -------------------------------------------------------------------------------- 1 | export const updateDelay = 250; 2 | export const editAnimationDuration = 600; 3 | export const editAnimationEasing = 'linear'; 4 | export const editAnimationColor = 'rgba(255, 255, 0, 0.5)'; 5 | export const editAnimationWhite = 'rgba(255, 255, 255, 0.5)'; 6 | export const associationSearchLimit = 30; 7 | export const tippyTopZIndex = 10001; 8 | export const tippyDefaults = { 9 | theme: 'light', 10 | placement: 'bottom', 11 | createPopperInstanceOnInit: true, 12 | animation: 'fade', 13 | animateFill: false, 14 | updateDuration: 250, 15 | duration: [ 250, 0 ], 16 | delay: [ 0, 0 ], 17 | hideDuration: 0, // necessary on tippy.js@2.0.9 18 | arrow: true, 19 | trigger: 'click', 20 | interactive: true, 21 | multiple: true, 22 | hideOnClick: true, 23 | dynamicInputDetection: true, 24 | zIndex: 9999, 25 | performance: true, 26 | touchHold: false, 27 | 28 | // These options should be enabled per-tippy, as needed 29 | sticky: false, 30 | livePlacement: false, 31 | }; 32 | -------------------------------------------------------------------------------- /src/client/dom.js: -------------------------------------------------------------------------------- 1 | function $( selector ){ 2 | return document.querySelector( selector ); 3 | } 4 | 5 | function $$( selector ){ 6 | return Array.from( document.querySelectorAll( selector ) ); 7 | } 8 | 9 | function makeClassList( obj ){ 10 | let getClass = k => obj[k] ? k : null; 11 | let nonNil = v => v != null; 12 | 13 | return Object.keys( obj ).map( getClass ).filter( nonNil ).join(' '); 14 | } 15 | 16 | function focusDomElement( el ){ 17 | el.focus(); 18 | 19 | if( typeof el.value === typeof '' ){ 20 | let len = el.value.length; 21 | 22 | el.setSelectionRange( len, len ); 23 | } 24 | } 25 | 26 | export { $, $$, makeClassList, focusDomElement }; 27 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import debug from './debug'; 2 | import Router from './router'; 3 | import ReactDom from 'react-dom'; 4 | import h from 'react-hyperscript'; 5 | import hh from 'hyperscript'; 6 | import { regCyExts } from '../util'; 7 | import { $ } from './dom'; 8 | import smoothscroll from 'smoothscroll-polyfill'; 9 | 10 | smoothscroll.polyfill(); // enable smooth scroll on safari 11 | 12 | // make sure cytoscape extensions are registered for the client side 13 | regCyExts(); 14 | 15 | if( debug.enabled() ){ 16 | debug.init(); 17 | } 18 | 19 | let rootDiv = hh('div#root'); 20 | let body = document.body; 21 | let hideInitter = () => { 22 | let el = $('.init-app'); 23 | 24 | if( el ){ 25 | el.classList.add('init-app-initted'); 26 | } 27 | }; 28 | 29 | body.appendChild( rootDiv ); 30 | 31 | ReactDom.render( h( Router ), rootDiv, hideInitter ); 32 | -------------------------------------------------------------------------------- /src/client/logger.js: -------------------------------------------------------------------------------- 1 | import { LOG_LEVEL } from '../config'; 2 | 3 | const LEVELS = Object.freeze({ 4 | info: 0, 5 | warn: 1, 6 | error: 2, 7 | none: 3 8 | }); 9 | 10 | let log = function( min, lvl, ...args ){ 11 | let i = LEVELS[ lvl ]; 12 | let n = LEVELS[ min ]; 13 | 14 | if( i >= n ){ 15 | console[ lvl ].call( console, ...args ); // eslint-disable-line no-console 16 | } 17 | }; 18 | 19 | let toMs = date => (date).valueOf(); 20 | 21 | class Logger { 22 | constructor( options ){ 23 | options = options || {}; 24 | 25 | this.minLevel = options.minLevel || 'info'; 26 | this.history = []; 27 | this.maxEntries = options.maxEntries || Infinity; 28 | } 29 | 30 | at( index ){ 31 | let entry = this.history[ index ]; 32 | 33 | log( this.minLevel, entry.level, ...entry.args ); 34 | } 35 | 36 | where( match ){ 37 | let hist = this.history; 38 | 39 | for( let i = 0; i < hist.length; i++ ){ 40 | let entry = hist[i]; 41 | 42 | if( match( entry, i ) ){ 43 | log( this.minLevel, entry.level, ...entry.args ); 44 | } 45 | } 46 | } 47 | 48 | first( n ){ 49 | this.where( (ent, i) => i < n ); 50 | } 51 | 52 | last( n ){ 53 | this.where( (ent, i) => i > this.history.length - 1 - n ); 54 | } 55 | 56 | before( date ){ 57 | date = toMs( date ); 58 | 59 | this.where( ent => ent.date <= date ); 60 | } 61 | 62 | after( date ){ 63 | date = toMs( date ); 64 | 65 | this.where( ent => ent.date >= date ); 66 | } 67 | 68 | all(){ 69 | this.where( () => true ); 70 | } 71 | 72 | clear(){ 73 | this.history.splice( 0, this.history.length ); 74 | } 75 | 76 | shift(){ 77 | let hist = this.history; 78 | 79 | hist.splice( 0, hist.length - this.maxEntries ); 80 | } 81 | 82 | static get LEVELS(){ return LEVELS; } 83 | } 84 | 85 | Object.keys( LEVELS ).forEach( lvl => { 86 | if( lvl === 'none' ){ return; } 87 | 88 | Logger.prototype[lvl] = function( ...args ){ 89 | log( this.minLevel, lvl, ...args ); 90 | 91 | let hist = this.history; 92 | 93 | hist.push({ 94 | date: Date.now(), 95 | args: args, 96 | level: lvl, 97 | levelCode: LEVELS[ lvl ] 98 | }); 99 | 100 | if( hist.length > this.maxEntries ){ 101 | this.shift(); 102 | } 103 | }; 104 | }); 105 | 106 | export default new Logger({ minLevel: LOG_LEVEL, maxEntries: 10 }); 107 | -------------------------------------------------------------------------------- /src/client/polyfills.js: -------------------------------------------------------------------------------- 1 | // TODO: es6 import 2 | import babelPoly from 'babel-polyfill'; // eslint-disable-line 3 | import fetchPoly from 'whatwg-fetch'; // eslint-disable-line 4 | -------------------------------------------------------------------------------- /src/client/router.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; 2 | import h from 'react-hyperscript'; 3 | import _ from 'lodash'; 4 | 5 | import PageNotFound from './components/page-not-found'; 6 | import Editor from './components/editor'; 7 | import Home from './components/home'; 8 | import DocumentManagement from './components/document-management'; 9 | 10 | import { DEMO_ID, DEMO_SECRET } from '../config'; 11 | 12 | 13 | let routes = [ 14 | { 15 | path: '/', 16 | render: (props) => { 17 | return h(Home, { history: props.history }); 18 | } 19 | }, 20 | { 21 | path: '/demo', 22 | render: props => { 23 | let id = DEMO_ID; 24 | let secret = DEMO_SECRET; 25 | let { history } = props; 26 | 27 | return h( Editor, { id, secret, history } ); 28 | } 29 | }, 30 | { 31 | path: '/admin', 32 | render: () => { 33 | return h( Redirect, { to: '/document' } ); 34 | } 35 | }, 36 | { 37 | path: '/document', 38 | render: props => { 39 | let { history } = props; 40 | 41 | return h( DocumentManagement, { history } ); 42 | } 43 | }, 44 | { 45 | path: '/document/:id', 46 | render: props => { 47 | let { id } = props.match.params; 48 | let { history } = props; 49 | 50 | return h( Editor, { id, history } ); 51 | } 52 | }, 53 | { 54 | path: '/document/:id/:secret', 55 | render: props => { 56 | let { id, secret } = props.match.params; 57 | let { history } = props; 58 | 59 | return h( Editor, { id, secret, history } ); 60 | } 61 | }, 62 | { 63 | render: () => { 64 | return h( PageNotFound ); 65 | }, 66 | status: 404 67 | } 68 | ].map( spec => { 69 | spec = _.defaults( spec, { 70 | exact: true 71 | } ); 72 | 73 | return h( Route, spec ); 74 | } ); 75 | 76 | export default () => ( 77 | h( BrowserRouter, [ 78 | h( Switch, routes ) 79 | ] ) 80 | ); 81 | -------------------------------------------------------------------------------- /src/model/document/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './document'; 2 | -------------------------------------------------------------------------------- /src/model/element-cache.js: -------------------------------------------------------------------------------- 1 | import { fill, assertFieldsDefined, getId, tryPromise } from '../util'; 2 | 3 | let defaults = { 4 | secret: 'read-only', 5 | factory: undefined // to get elements not in the cache 6 | }; 7 | 8 | /** 9 | A cache for biological elements. 10 | 11 | If an element does not exist in the cache when queried, it will be loaded via the 12 | specified source and added to the cache. 13 | 14 | The cache prevents having to make queries to the server/DB for elements that are 15 | already loaded (e.g. in the same document). It also prevents double loading for 16 | interactions. 17 | */ 18 | class ElementCache { 19 | constructor( opts = {} ){ 20 | assertFieldsDefined( opts, ['factory'] ); 21 | 22 | fill({ 23 | obj: this, 24 | from: opts, 25 | defs: defaults 26 | }); 27 | 28 | this.source = new Map(); 29 | } 30 | 31 | has( id ){ 32 | return this.source.has( id ); 33 | } 34 | 35 | get( id ){ 36 | return this.source.get( id ); 37 | } 38 | 39 | add( ele ){ 40 | this.source.set( ele.id(), ele ); 41 | } 42 | 43 | remove( ele ){ 44 | this.source.delete( getId( ele ) ); 45 | } 46 | 47 | reload( ele ){ 48 | let id = getId( ele ); 49 | 50 | this.remove( ele ); 51 | 52 | return this.load( id ); 53 | } 54 | 55 | load( id, opts ){ 56 | let secret = this.secret; 57 | let get = () => this.get( id ); 58 | let load = () => this.factory.load({ cache: this, data: { id, secret } }); 59 | let add = ele => this.add( ele, opts ); 60 | let loadAndAddIfNoEle = ele => { 61 | if( ele ){ return ele; } 62 | 63 | return load().then( loadedEle => { 64 | add( loadedEle ); 65 | 66 | return loadedEle; 67 | } ); 68 | }; 69 | 70 | return tryPromise( get ).then( loadAndAddIfNoEle ); 71 | } 72 | } 73 | 74 | export default ElementCache; 75 | -------------------------------------------------------------------------------- /src/model/element/element-type.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { INTERACTION_TYPE_VALS as INTERACTION_TYPE } from './interaction-type/enum'; 3 | import { ENTITY_TYPE, ENTITY_TYPES } from './entity-type'; 4 | 5 | const INTERACTION_TYPES = _.flatMap( INTERACTION_TYPE ); 6 | 7 | const ELEMENT_TYPE = Object.assign({}, ENTITY_TYPE, INTERACTION_TYPE); 8 | 9 | const ELEMENT_TYPES = _.flatMap( ELEMENT_TYPE ); 10 | 11 | const isEntity = type => { 12 | return ENTITY_TYPES.indexOf(type) >= 0; 13 | }; 14 | 15 | const isInteraction = type => { 16 | return INTERACTION_TYPES.indexOf(type) >= 0; 17 | }; 18 | 19 | const isComplex = type => { 20 | return type == ENTITY_TYPE.COMPLEX; 21 | }; 22 | 23 | const isGGP = type => { 24 | return ( type === ENTITY_TYPE.GGP 25 | || type === ENTITY_TYPE.DNA 26 | || type === ENTITY_TYPE.RNA 27 | || type === ENTITY_TYPE.PROTEIN 28 | ); 29 | }; 30 | 31 | export { ELEMENT_TYPE, ELEMENT_TYPES, isEntity, isInteraction, isComplex, isGGP }; 32 | -------------------------------------------------------------------------------- /src/model/element/entity-type.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const ENTITY_TYPE = Object.freeze({ 4 | ENTITY: 'entity', 5 | GGP: 'ggp', // gene or gene product 6 | DNA: 'dna', 7 | RNA: 'rna', 8 | PROTEIN: 'protein', 9 | CHEMICAL: 'chemical', 10 | COMPLEX: 'complex', 11 | NAMED_COMPLEX: 'namedComplex' 12 | }); 13 | 14 | const NCBI_GENE_TYPE = Object.freeze({ 15 | GGP: ['unknown', 'biological-region', 'other'], 16 | RNA: ['tRNA', 'rRNA', 'snRNA', 'scRNA', 'snoRNA', 'miscRNA', 'ncRNA'], 17 | DNA: ['pseudo', 'transposon'], 18 | PROTEIN: ['protein-coding'] 19 | }); 20 | 21 | const getNCBIEntityType = typeOfGene => { 22 | let keys = Object.keys( NCBI_GENE_TYPE ); 23 | let res = null; 24 | 25 | for ( let i = 0; i < keys.length; i++ ) { 26 | let key = keys[i]; 27 | if ( _.includes( NCBI_GENE_TYPE[ key ], typeOfGene ) ) { 28 | res = ENTITY_TYPE[ key ]; 29 | break; 30 | } 31 | } 32 | 33 | return res; 34 | }; 35 | 36 | const ENTITY_TYPES = _.flatMap( ENTITY_TYPE ); 37 | 38 | export { ENTITY_TYPE, ENTITY_TYPES, getNCBIEntityType }; 39 | -------------------------------------------------------------------------------- /src/model/element/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './factory'; 2 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/binding.js: -------------------------------------------------------------------------------- 1 | import InteractionType from './interaction-type'; 2 | import { PARTICIPANT_TYPE } from '../participant-type'; 3 | import { BIOPAX_TEMPLATE_TYPE } from './biopax-type'; 4 | import _ from 'lodash'; 5 | 6 | const VALUE = 'binding'; 7 | const DISPLAY_VALUE = 'Binding'; 8 | 9 | const allowedParticipantTypes = () => { 10 | const T = PARTICIPANT_TYPE; 11 | 12 | return [ T.UNSIGNED ]; 13 | }; 14 | 15 | class Binding extends InteractionType { 16 | constructor( intn ){ 17 | super( intn ); 18 | } 19 | 20 | isComplete() { 21 | return !this.isSigned() && Binding.isAllowedForInteraction(this.interaction); 22 | } 23 | 24 | static allowedParticipantTypes(){ 25 | return allowedParticipantTypes(); 26 | } 27 | 28 | allowedParticipantTypes(){ 29 | return allowedParticipantTypes(); 30 | } 31 | 32 | static isAllowedForInteraction( intn ){ 33 | // TODO: is complex okay? 34 | let ppts = intn.participants(); 35 | return ppts.length === 2; 36 | } 37 | 38 | toBiopaxTemplate( transform, omitDbXref ){ 39 | if ( !this.validatePpts() ){ 40 | return this.makeInvalidBiopaxTemplate( transform, omitDbXref ); 41 | } 42 | 43 | let participants = _.uniqBy(this.interaction.participants().map( transform ), p => p.id() ); 44 | 45 | // if only one participant is remained after the transformation skip the interaction 46 | if ( participants.length == 1 ) { 47 | return null; 48 | } 49 | 50 | let participantTemplates = participants.map( participant => participant.toBiopaxTemplate( omitDbXref ) ); 51 | 52 | return { 53 | type: BIOPAX_TEMPLATE_TYPE.MOLECULAR_INTERACTION, 54 | participants: participantTemplates 55 | }; 56 | } 57 | 58 | toString(){ 59 | if( this.isNegative() || this.isPositive() ){ 60 | return super.toString(null, 'via binding'); 61 | } else { 62 | return super.toString('binds with'); 63 | } 64 | } 65 | 66 | static get value(){ return VALUE; } 67 | get value(){ return VALUE; } 68 | 69 | static get displayValue(){ return DISPLAY_VALUE; } 70 | get displayValue(){ return DISPLAY_VALUE; } 71 | } 72 | 73 | export default Binding; 74 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/biopax-type.js: -------------------------------------------------------------------------------- 1 | const BIOPAX_TEMPLATE_TYPE = Object.freeze({ 2 | //case: 1 (intn. type: Binding) 3 | MOLECULAR_INTERACTION: 'Molecular Interaction', 4 | // case: 2 (TranscriptionTranslation) 5 | EXPRESSION_REGULATION: 'Expression Regulation', 6 | //case: 3 (Modification affects active/inactive state, possibly via e.g. ubiquitination) 7 | PROTEIN_CONTROLS_STATE: 'Protein Controls State', 8 | //case: 4A-E; biopax converter results are based on the ent. types, order, sign. 9 | OTHER_INTERACTION: 'Other Interaction', 10 | }); 11 | 12 | const BIOPAX_CONTROL_TYPE = Object.freeze({ 13 | INHIBITION: 'inhibition', 14 | ACTIVATION: 'activation' 15 | }); 16 | 17 | export { BIOPAX_TEMPLATE_TYPE, BIOPAX_CONTROL_TYPE }; 18 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/demethylation.js: -------------------------------------------------------------------------------- 1 | import Modification from './modification'; 2 | 3 | const VALUE = 'demethylation'; 4 | const DISPLAY_VALUE = 'Demethylation'; 5 | const EFFECT = 'demethylated'; 6 | 7 | class Demethylation extends Modification { 8 | constructor( intn ){ 9 | super( intn ); 10 | } 11 | 12 | toBiopaxTemplate(transform, omitDbXref){ 13 | return super.toBiopaxTemplate(transform, omitDbXref, EFFECT); 14 | } 15 | 16 | toString(){ 17 | return super.toString(VALUE); 18 | } 19 | 20 | static get value(){ return VALUE; } 21 | get value(){ return VALUE; } 22 | 23 | static get displayValue(){ return DISPLAY_VALUE; } 24 | get displayValue(){ return DISPLAY_VALUE; } 25 | } 26 | 27 | export default Demethylation; 28 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/dephosphorylation.js: -------------------------------------------------------------------------------- 1 | import Modification from './modification'; 2 | 3 | const VALUE = 'dephosphorylation'; 4 | const DISPLAY_VALUE = 'Dephosphorylation'; 5 | const EFFECT = 'dephosphorylated'; 6 | 7 | class Dephosphorylation extends Modification { 8 | constructor( intn ){ 9 | super( intn ); 10 | } 11 | 12 | toBiopaxTemplate(transform, omitDbXref){ 13 | return super.toBiopaxTemplate(transform, omitDbXref, EFFECT); 14 | } 15 | 16 | toString(){ 17 | return super.toString('dephosphorylation'); 18 | } 19 | 20 | static get value(){ return VALUE; } 21 | get value(){ return VALUE; } 22 | 23 | static get displayValue(){ return DISPLAY_VALUE; } 24 | get displayValue(){ return DISPLAY_VALUE; } 25 | } 26 | 27 | export default Dephosphorylation; 28 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/deubiquitination.js: -------------------------------------------------------------------------------- 1 | import Modification from './modification'; 2 | 3 | const VALUE = 'deubiquitination'; 4 | const DISPLAY_VALUE = 'Deubiquitination'; 5 | const EFFECT = 'deubiquitinated'; 6 | 7 | class Deubiquitination extends Modification { 8 | constructor( intn ){ 9 | super( intn ); 10 | } 11 | 12 | toBiopaxTemplate(transform, omitDbXref){ 13 | return super.toBiopaxTemplate(transform, omitDbXref, EFFECT); 14 | } 15 | 16 | toString(){ 17 | return super.toString(VALUE); 18 | } 19 | 20 | static get value(){ return VALUE; } 21 | get value(){ return VALUE; } 22 | 23 | static get displayValue(){ return DISPLAY_VALUE; } 24 | get displayValue(){ return DISPLAY_VALUE; } 25 | } 26 | 27 | export default Deubiquitination; 28 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/enum.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import Interaction from './interaction'; 4 | import Binding from './binding'; 5 | import TranscriptionTranslation from './transcription-translation'; 6 | import Modification from './modification'; 7 | import Phosphorylation from './phosphorylation'; 8 | import Dephosphorylation from './dephosphorylation'; 9 | import Methylation from './methylation'; 10 | import Demethylation from './demethylation'; 11 | import Ubiquitination from './ubiquitination'; 12 | import Deubiquitination from './deubiquitination'; 13 | 14 | const INTERACTION_TYPE = Object.freeze({ 15 | BINDING: Binding, 16 | TRANSCRIPTION_TRANSLATION: TranscriptionTranslation, 17 | MODIFICATION: Modification, 18 | PHOSPHORYLATION: Phosphorylation, 19 | DEPHOSPHORYLATION: Dephosphorylation, 20 | METHYLATION: Methylation, 21 | DEMETHYLATION: Demethylation, 22 | UBIQUITINATION: Ubiquitination, 23 | DEUBIQUITINATION: Deubiquitination, 24 | INTERACTION: Interaction // other / catch-all 25 | }); 26 | 27 | const INTERACTION_TYPE_VALS = ( () => { 28 | let keys = _.keys( INTERACTION_TYPE ); 29 | let vals = {}; 30 | 31 | keys.forEach( key => { 32 | let val = INTERACTION_TYPE[key].value; 33 | vals[ key ] = val; 34 | } ); 35 | 36 | return vals; 37 | } )(); 38 | 39 | const INTERACTION_TYPES = _.keys( INTERACTION_TYPE ).map( k => INTERACTION_TYPE[k] ); 40 | 41 | const getIntnTypeByVal = val => { 42 | return INTERACTION_TYPES.find( type => type.value === val ) || INTERACTION_TYPE.GENERAL; 43 | }; 44 | 45 | export { INTERACTION_TYPE, INTERACTION_TYPES, getIntnTypeByVal, INTERACTION_TYPE_VALS }; 46 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/index.js: -------------------------------------------------------------------------------- 1 | export * from './enum'; 2 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/interaction.js: -------------------------------------------------------------------------------- 1 | import InteractionType from './interaction-type'; 2 | import { BIOPAX_TEMPLATE_TYPE, BIOPAX_CONTROL_TYPE } from './biopax-type'; 3 | import _ from 'lodash'; 4 | 5 | const VALUE = 'interaction'; 6 | const DISPLAY_VALUE = 'Other'; 7 | 8 | class Interaction extends InteractionType { 9 | 10 | constructor( intn ){ 11 | super( intn ); 12 | } 13 | 14 | static isAllowedForInteraction( intn ){ 15 | return intn.participants().length === 2; 16 | } 17 | 18 | isComplete() { 19 | return Interaction.isAllowedForInteraction(this.interaction); 20 | } 21 | 22 | toBiopaxTemplate( transform, omitDbXref ){ 23 | if ( !this.validatePpts( transform ) ){ 24 | return this.makeInvalidBiopaxTemplate( transform, omitDbXref ); 25 | } 26 | 27 | // "Other" type; see 4A-E in "Factoid binary interaction types" doc. 28 | let template = { 29 | type: BIOPAX_TEMPLATE_TYPE.OTHER_INTERACTION 30 | }; 31 | 32 | //optional controlType 33 | if(this.isNegative()) 34 | template.controlType = BIOPAX_CONTROL_TYPE.INHIBITION; 35 | else if(this.isPositive()) 36 | template.controlType = BIOPAX_CONTROL_TYPE.ACTIVATION; 37 | 38 | //optional source, target are either both null or both defined (unless there is a bug) 39 | let source = this.getSource(); 40 | let target = this.getTarget(); 41 | //ensure participants order is always [source,target] if defined 42 | let participants = (source && target) ? [source, target] : this.interaction.participants(); 43 | participants = _.uniqBy( participants.map( transform ), p => p.id() ); 44 | 45 | // if only one participant is remained after the transformation skip the interaction 46 | if ( participants.length == 1 ) { 47 | return null; 48 | } 49 | 50 | template.participants = participants.map( participant => participant.toBiopaxTemplate( omitDbXref ) ); 51 | 52 | return template; 53 | } 54 | 55 | static get value(){ return VALUE; } 56 | get value(){ return VALUE; } 57 | 58 | static get displayValue(){ return DISPLAY_VALUE; } 59 | get displayValue(){ return DISPLAY_VALUE; } 60 | } 61 | 62 | export default Interaction; 63 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/methylation.js: -------------------------------------------------------------------------------- 1 | import Modification from './modification'; 2 | 3 | const VALUE = 'methylation'; 4 | const DISPLAY_VALUE = 'Methylation'; 5 | const EFFECT = 'methylated'; 6 | 7 | class Methylation extends Modification { 8 | constructor( intn ){ 9 | super( intn ); 10 | } 11 | 12 | toBiopaxTemplate( transform, omitDbXref ){ 13 | return super.toBiopaxTemplate(transform, omitDbXref, EFFECT); 14 | } 15 | 16 | toString(){ 17 | return super.toString(VALUE); 18 | } 19 | 20 | static get value(){ return VALUE; } 21 | get value(){ return VALUE; } 22 | 23 | static get displayValue(){ return DISPLAY_VALUE; } 24 | get displayValue(){ return DISPLAY_VALUE; } 25 | } 26 | 27 | export default Methylation; 28 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/phosphorylation.js: -------------------------------------------------------------------------------- 1 | import Modification from './modification'; 2 | 3 | const VALUE = 'phosphorylation'; 4 | const DISPLAY_VALUE = 'Phosphorylation'; 5 | const EFFECT = 'phosphorylated'; 6 | 7 | class Phosphorylation extends Modification { 8 | constructor( intn ){ 9 | super( intn ); 10 | } 11 | 12 | toBiopaxTemplate( transform, omitDbXref ){ 13 | return super.toBiopaxTemplate(transform, omitDbXref, EFFECT); 14 | } 15 | 16 | toString(){ 17 | return super.toString('phosphorylation'); 18 | } 19 | 20 | static get value(){ return VALUE; } 21 | get value(){ return VALUE; } 22 | 23 | static get displayValue(){ return DISPLAY_VALUE; } 24 | get displayValue(){ return DISPLAY_VALUE; } 25 | } 26 | 27 | export default Phosphorylation; 28 | -------------------------------------------------------------------------------- /src/model/element/interaction-type/ubiquitination.js: -------------------------------------------------------------------------------- 1 | import Modification from './modification'; 2 | 3 | const VALUE = 'ubiquitination'; 4 | const DISPLAY_VALUE = 'Ubiquitination'; 5 | const EFFECT = 'ubiquitinated'; 6 | 7 | class Ubiquitination extends Modification { 8 | constructor( intn ){ 9 | super( intn ); 10 | } 11 | 12 | toBiopaxTemplate( transform, omitDbXref ){ 13 | return super.toBiopaxTemplate(transform, omitDbXref, EFFECT); 14 | } 15 | 16 | toString(){ 17 | return super.toString(VALUE); 18 | } 19 | 20 | static get value(){ return VALUE; } 21 | get value(){ return VALUE; } 22 | 23 | static get displayValue(){ return DISPLAY_VALUE; } 24 | get displayValue(){ return DISPLAY_VALUE; } 25 | } 26 | 27 | export default Ubiquitination; 28 | -------------------------------------------------------------------------------- /src/model/element/participant-type.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const pptType = ( value, displayValue, icon, verbPhrase ) => Object.freeze({ value, displayValue, icon, verbPhrase }); 4 | 5 | const PARTICIPANT_TYPE = Object.freeze({ 6 | UNSIGNED: pptType('unsigned', 'unsigned', 'icon icon-arrow-unsigned', 'interacts with'), 7 | POSITIVE: pptType('positive', 'positive', 'icon icon-arrow-positive', 'activates'), 8 | NEGATIVE: pptType('negative', 'negative', 'icon icon-arrow-negative', 'inhibits') 9 | }); 10 | 11 | //pptType objects array 12 | const PARTICIPANT_TYPES = _.keys( PARTICIPANT_TYPE ).map( k => PARTICIPANT_TYPE[k] ); 13 | 14 | //finds a pptType by value (e.g., 'positive') 15 | const getPptTypeByVal = val => { 16 | return PARTICIPANT_TYPES.find( type => type.value === val ) || PARTICIPANT_TYPE.UNSIGNED; 17 | }; 18 | 19 | export { PARTICIPANT_TYPE, PARTICIPANT_TYPES, getPptTypeByVal }; 20 | -------------------------------------------------------------------------------- /src/model/empty-element-cache.js: -------------------------------------------------------------------------------- 1 | import ElementCache from './element-cache'; 2 | 3 | class EmptyElementCache extends ElementCache { 4 | constructor( opts ){ 5 | super( opts ); 6 | } 7 | 8 | has(){ 9 | return false; 10 | } 11 | 12 | get(){ 13 | return undefined; 14 | } 15 | 16 | add(){} 17 | 18 | remove(){} 19 | } 20 | 21 | export default EmptyElementCache; 22 | -------------------------------------------------------------------------------- /src/model/event-emitter-mixin.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import _ from 'lodash'; 3 | let m; 4 | 5 | /** 6 | Allows for direct EventEmitter functions on a class, while using only the `emitter` 7 | property on the class. 8 | */ 9 | let EventEmitterMixin = m = function(){ 10 | this.emitter = new EventEmitter(); 11 | this.forwardedEmitters = []; 12 | }; 13 | 14 | _.extend( m.prototype, { 15 | on: function( name, listener ){ 16 | this.emitter.on( name, listener, this ); 17 | 18 | return this; 19 | }, 20 | 21 | once: function( name, listener ){ 22 | this.emitter.once( name, listener, this ); 23 | 24 | return this; 25 | }, 26 | 27 | removeListener: function( name, listener ){ 28 | this.emitter.removeListener( name, listener, this ); 29 | 30 | return this; 31 | }, 32 | 33 | removeAllListeners: function( name ){ 34 | this.emitter.removeAllListeners( name ); 35 | 36 | return this; 37 | }, 38 | 39 | emit: function( name, ...args ){ 40 | let emit = ee => ee.emit( name, ...args ); 41 | 42 | emit( this.emitter ); 43 | this.forwardedEmitters.forEach( emit ); 44 | 45 | return this; 46 | }, 47 | 48 | forward: function( ee ){ 49 | this.forwardedEmitters.push( ee ); 50 | 51 | return this; 52 | }, 53 | 54 | removeForward: function( ee ){ 55 | _.pull( this.forwardedEmitters, ee ); 56 | } 57 | }); 58 | 59 | _.extend( m, { 60 | addListener: m.on, 61 | off: m.removeListener, 62 | trigger: m.emit, 63 | unforward: m.removeForward 64 | } ); 65 | 66 | export default EventEmitterMixin; 67 | -------------------------------------------------------------------------------- /src/neo4j/index.js: -------------------------------------------------------------------------------- 1 | export { addDocumentToNeo4j } from './neo4j-document.js'; 2 | export { addNode, addEdge, neighbourhood, deleteAllNodesAndEdges } from './neo4j-functions.js'; -------------------------------------------------------------------------------- /src/neo4j/neo4j-driver.js: -------------------------------------------------------------------------------- 1 | import neo4j from 'neo4j-driver'; 2 | import { GRAPHDB_CONN, GRAPHDB_USER, GRAPHDB_PASS, GRAPHDB_DBNAME } from '../config.js'; 3 | const DEFAULT_SESSION_CONFIG = { database: GRAPHDB_DBNAME }; 4 | 5 | let driver; 6 | 7 | /** 8 | * Initialize the Neo4j driver singleton 9 | * 10 | * @param {object} config additional configuration 11 | * @returns Neo4j {@link https://neo4j.com/docs/api/javascript-driver/current/class/lib6/driver.js~Driver.html Driver} instance 12 | */ 13 | export function initDriver( config = {} ) { 14 | driver = neo4j.driver( GRAPHDB_CONN, 15 | neo4j.auth.basic( GRAPHDB_USER, GRAPHDB_PASS ), 16 | config 17 | ); 18 | return driver; 19 | } 20 | 21 | /** 22 | * Retrieve the Neo4j driver instance 23 | * 24 | * @returns The Neo4j Driver instance 25 | */ 26 | export function getDriver() { 27 | return driver; 28 | } 29 | 30 | /** 31 | * Close the Neo4j driver instance 32 | * 33 | * @returns Promise 34 | */ 35 | export async function closeDriver() { 36 | return driver && driver.close(); 37 | } 38 | 39 | /** 40 | * Convenience function for accessing a session 41 | * 42 | * @param {object} sessionConfig additional configuration 43 | * @returns A neo4j driver {@link https://neo4j.com/docs/api/javascript-driver/current/class/lib6/session.js~Session.html Session} instance 44 | */ 45 | export function guaranteeSession( sessionConfig = DEFAULT_SESSION_CONFIG ) { 46 | if ( !driver ) initDriver(); 47 | return driver.session( sessionConfig ); 48 | } 49 | -------------------------------------------------------------------------------- /src/neo4j/query-strings.js: -------------------------------------------------------------------------------- 1 | // The following string templates are Cypher query strings 2 | 3 | export const constraint = `CREATE CONSTRAINT IF NOT EXISTS FOR (x:Entity) REQUIRE x.id IS UNIQUE`; 4 | 5 | export const makeNodeQuery = 6 | `MATCH (n:Entity {id: $id}) 7 | WITH collect(n) AS nodes 8 | CALL apoc.lock.nodes(nodes) 9 | MERGE (n:Entity {id: $id}) 10 | ON CREATE SET n.name = $name`; 11 | 12 | export const makeEdgeQuery = 13 | `MATCH (x:Entity {id: $sourceId})-[r:INTERACTION]->(y:Entity {id: $targetId}) 14 | WITH collect(r) AS relationships 15 | CALL apoc.lock.rels(relationships) 16 | MATCH (x:Entity {id: $sourceId}) 17 | MATCH (y:Entity {id: $targetId}) 18 | MERGE (x)-[r:INTERACTION {id: $id}]->(y) 19 | ON CREATE SET r.type = $type, 20 | r.group = $group, 21 | r.component = $component, 22 | r.sourceId = $sourceId, 23 | r.targetId = $targetId, 24 | r.sourceComplex = $sourceComplex, 25 | r.targetComplex = $targetComplex, 26 | r.xref = $xref, 27 | r.doi = $doi, 28 | r.pmid = $pmid, 29 | r.articleTitle = $articleTitle`; 30 | 31 | export const giveConnectedInfoByGeneId = 32 | `MATCH (n:Entity {id: $id})<-[r]-(m) 33 | RETURN n, r, m 34 | UNION 35 | MATCH (n:Entity {id: $id})-[r]->(m) 36 | RETURN n, r, m`; 37 | 38 | export const giveConnectedInfoByGeneIdNoComplexes = 39 | `MATCH (n:Entity {id: $id})<-[r]-(m) 40 | WHERE r.component = [] AND r.sourceComplex = '' AND r.targetComplex = '' 41 | RETURN n, r, m 42 | UNION 43 | MATCH (n:Entity {id: $id})-[r]->(m) 44 | WHERE r.component = [] AND r.sourceComplex = '' AND r.targetComplex = '' 45 | RETURN n, r, m`; 46 | 47 | export const giveConnectedInfoForDocument = 48 | `MATCH (n)-[r {xref: $id}]->(m) 49 | RETURN n, r, m`; 50 | 51 | export const returnGene = 52 | `MATCH (n {id: $id}) 53 | RETURN n`; 54 | 55 | export const returnEdgeById = 56 | `MATCH(n)-[r {id: $id}]->(m) 57 | RETURN r`; 58 | 59 | export const returnEdgeByIdAndEndpoints = 60 | `MATCH(n {id: $sourceId})-[r {id: $complexId}]->(m {id: $targetId}) 61 | RETURN r`; 62 | 63 | export const deleteAll = `MATCH (n) DETACH DELETE n`; 64 | 65 | export const numNodes = `MATCH (n) RETURN COUNT(*) as count`; 66 | 67 | export const numEdges = `MATCH (n)-[r]->(m) RETURN COUNT(r) as count`; -------------------------------------------------------------------------------- /src/server/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import * as config from '../config'; 3 | 4 | let logger = new (winston.Logger)({ 5 | transports: [ 6 | new (winston.transports.Console)({ level: config.LOG_LEVEL }), 7 | new (winston.transports.File)({ name: 'debug', filename: 'debug.log', level: 'debug' }), 8 | new (winston.transports.File)({ name: 'warn', filename: 'warn.log', level: 'warn' }), 9 | new (winston.transports.File)({ name: 'error', filename: 'error.log', level: 'error' }) 10 | ] 11 | }); 12 | 13 | logger.cli(); 14 | 15 | export default logger; 16 | -------------------------------------------------------------------------------- /src/server/routes/api/document/cache.js: -------------------------------------------------------------------------------- 1 | import NodeCache from 'node-cache'; 2 | import cron from 'node-cron'; 3 | 4 | import { CRON_SCHEDULE_DOCCACHE_UPDATE } from '../../../../config.js'; 5 | import { getDocuments } from '.'; 6 | import logger from '../../../logger'; 7 | 8 | const cache = new NodeCache(); 9 | 10 | const DOCCACHE_KEY = Object.freeze({ 11 | SEARCH: 'search', 12 | LATEST: 'latest' 13 | }); 14 | 15 | let docCache = { 16 | /** 17 | * Set or replace the Document cache 18 | * @param {string} key - The key to retrieve from the cache 19 | */ 20 | update: async function( key, opts = {}) { 21 | try { 22 | logger.info( `Started update of Document cache` ); 23 | const data = await getDocuments( opts ); 24 | cache.set( key, data ); 25 | logger.info( `Completed update of Document cache` ); 26 | } catch( err ){ 27 | logger.error( `Error updating cache with Documents: ${err.message}` ); // swallow 28 | } 29 | }, 30 | 31 | /** 32 | * Get a value from the Document cache 33 | * @param {string} key - The key to retrieve from the cache 34 | */ 35 | get: async function( key ) { 36 | try { 37 | logger.info( `Get from cache with ${key}` ); 38 | let exists = cache.has( key ); 39 | if( !exists ){ 40 | let opts = {}; 41 | if( key === DOCCACHE_KEY.SEARCH ){ 42 | opts = { limit: null }; 43 | } 44 | await this.update( key, opts ); 45 | } 46 | return cache.get( key ); 47 | } catch( err ){ 48 | logger.error( `Error getting from cache: ${err.message}` ); // swallow 49 | } 50 | }, 51 | 52 | delete: function( key ){ 53 | return cache.del( key ); 54 | } 55 | 56 | }; 57 | 58 | cron.schedule( CRON_SCHEDULE_DOCCACHE_UPDATE, async () => { 59 | logger.debug( `Running Document cache update cron job` ); 60 | await docCache.update( DOCCACHE_KEY.LATEST, {} ); 61 | await docCache.update( DOCCACHE_KEY.SEARCH, { limit: null } ); 62 | }); 63 | 64 | export { 65 | docCache, 66 | DOCCACHE_KEY 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /src/server/routes/api/document/crossref/index.js: -------------------------------------------------------------------------------- 1 | export * from './map.js'; 2 | export * from './works.js'; 3 | export * from './api.js'; 4 | -------------------------------------------------------------------------------- /src/server/routes/api/document/null.js: -------------------------------------------------------------------------------- 1 | export const get = function(){ 2 | return Promise.resolve({ 3 | elements: [], 4 | organisms: [] 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/server/routes/api/document/pubmed/linkPubmed.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import queryString from 'query-string'; 3 | import fetch from 'node-fetch'; 4 | import { URLSearchParams } from 'url'; 5 | 6 | import { NCBI_EUTILS_BASE_URL, NCBI_EUTILS_API_KEY } from '../../../../../config'; 7 | import { checkHTTPStatus } from '../../../../../util'; 8 | 9 | const DEFAULT_MAX_PER_LINK = 50; // Take the top n from each category 10 | const EUTILS_LINK_URL = NCBI_EUTILS_BASE_URL + 'elink.fcgi'; 11 | const DEFAULT_ELINK_PARAMS = { 12 | term: undefined, 13 | db: 'pubmed', 14 | dbfrom: 'pubmed', 15 | retmode: 'json', 16 | cmd: 'neighbor', 17 | api_key: NCBI_EUTILS_API_KEY, 18 | datetype: 'pdat', 19 | reldate: undefined, 20 | linkname: undefined 21 | }; 22 | 23 | /** 24 | * elink2UidList 25 | * Retrieve PubMed uids from the ELINK response 26 | * @param {Object} json The ELINK response 27 | * @param {Object} linknames The list linknames to consider 28 | * @param {number} maxPerLink Take this top number of uids for any one subset (dbfrom_db_subset) 29 | * @return {Object} The array of PubMed uids 30 | */ 31 | const elink2UidList = ( json, linknames, maxPerLink = DEFAULT_MAX_PER_LINK ) => { 32 | let { ids, linksetdbs = [] } = _.get( json, ['linksets', '0'] ); 33 | if( linknames ) linksetdbs = linksetdbs.filter( linksetdb => _.includes( linknames, linksetdb.linkname ) ); 34 | const links = linksetdbs.map( linksetdb => _.take( _.get( linksetdb, ['links'] ), maxPerLink ) ); 35 | let uids = _.flatten( links ); 36 | uids = _.uniq( uids ); // Remove redundancy 37 | uids = _.pullAll( uids, ids ); // Remove the query uids 38 | return uids; 39 | }; 40 | 41 | /** 42 | * eLink 43 | * Generic wrapper for NCBI ELINK EUTIL 44 | * @param {Object} opts The options for ELINK service(see [SML Dataguide]{@link https://dataguide.nlm.nih.gov/eutilities/utilities.html#elink} ) 45 | */ 46 | const eLink = opts => { 47 | const url = EUTILS_LINK_URL; 48 | const userAgent = `${process.env.npm_package_name}/${process.env.npm_package_version}`; 49 | const params = _.defaults( {}, opts, DEFAULT_ELINK_PARAMS ); 50 | const body = new URLSearchParams( queryString.stringify( params ) ); 51 | return fetch( url, { 52 | method: 'POST', 53 | headers: { 54 | 'User-Agent': userAgent 55 | }, 56 | body 57 | }) 58 | .then( checkHTTPStatus ) // HTTPStatusError 59 | .then( response => response.json() ); 60 | }; 61 | 62 | export { eLink, elink2UidList }; -------------------------------------------------------------------------------- /src/server/routes/api/document/pubmed/searchPubmed.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import queryString from 'query-string'; 3 | import fetch from 'node-fetch'; 4 | 5 | import { NCBI_EUTILS_BASE_URL, NCBI_EUTILS_API_KEY } from '../../../../../config'; 6 | import { checkHTTPStatus } from '../../../../../util'; 7 | 8 | const EUTILS_SEARCH_URL = NCBI_EUTILS_BASE_URL + 'esearch.fcgi'; 9 | const DEFAULT_ESEARCH_PARAMS = { 10 | term: undefined, 11 | db: 'pubmed', 12 | rettype: 'uilist', 13 | retmode: 'json', 14 | retmax: 10, 15 | usehistory: 'y', 16 | field: undefined, 17 | api_key: NCBI_EUTILS_API_KEY 18 | }; 19 | 20 | const pubmedDataConverter = json => { 21 | 22 | const esearchresult = _.get( json, ['esearchresult'] ); 23 | 24 | return { 25 | searchHits: _.get( esearchresult, ['idlist'], [] ), 26 | count: _.parseInt( _.get( esearchresult, ['count'], '0' ) ), 27 | query_key: _.get( esearchresult, ['querykey'], null ), 28 | webenv: _.get( esearchresult, ['webenv'], null ) 29 | }; 30 | }; 31 | 32 | const checkEsearchResult = json => { 33 | const errorMessage = _.get( json, ['esearchresult', 'ERROR'] ); 34 | if( errorMessage ) throw new Error( errorMessage ); 35 | return json; 36 | }; 37 | 38 | const eSearchPubmed = ( term, opts ) => { 39 | const params = _.assign( {}, DEFAULT_ESEARCH_PARAMS, { term }, opts ); 40 | const url = EUTILS_SEARCH_URL + '?' + queryString.stringify( params ); 41 | const userAgent = `${process.env.npm_package_name}/${process.env.npm_package_version}`; 42 | return fetch( url, { 43 | method: 'GET', 44 | headers: { 45 | 'User-Agent': userAgent 46 | } 47 | }) 48 | .then( checkHTTPStatus ) // HTTPStatusError 49 | .then( response => response.json() ) 50 | .then( checkEsearchResult ) // Error (programmatic) 51 | .then( pubmedDataConverter ); 52 | }; 53 | 54 | /** 55 | * searchPubmed 56 | * Query the PubMed database for matching UIDs. 57 | * 58 | * @param { String } q The query term 59 | * @param { Object } opts EUTILS ESEARCH options 60 | * @returns { Object } result The search results from PubMed 61 | * @returns { Array } result.searchHits A list of PMIDs 62 | * @returns { Number } result.count The number of searchHits containing PMIDs 63 | * @returns { String } result.query_key See {@link https://www.ncbi.nlm.nih.gov/books/NBK25499/#chapter4.ESearch|EUTILS docs } 64 | * @returns { String } result.webenv See {@link https://www.ncbi.nlm.nih.gov/books/NBK25499/#chapter4.ESearch|EUTILS docs } 65 | */ 66 | const searchPubmed = ( q, opts ) => eSearchPubmed( q, opts ); 67 | 68 | export { searchPubmed, pubmedDataConverter }; -------------------------------------------------------------------------------- /src/server/routes/api/document/related-papers-queue.js: -------------------------------------------------------------------------------- 1 | import {default as PQueue} from 'p-queue'; 2 | import delay from 'delay'; 3 | 4 | const pcQueue = new PQueue({concurrency: 1}); 5 | const adminQueue = new PQueue({concurrency: 1}); 6 | 7 | const addJob = async (relPprsFcn, queue) => { 8 | await queue.add(() => relPprsFcn().then( () => delay( 500 ) )); 9 | return Promise.resolve(); 10 | }; 11 | 12 | class AdminPapersQueue { 13 | static async addJob(relPprsFcn){ 14 | await addJob( relPprsFcn, adminQueue ); 15 | return Promise.resolve(); 16 | } 17 | } 18 | 19 | class PCPapersQueue { 20 | static async addJob(relPprsFcn){ 21 | await addJob( relPprsFcn, pcQueue ); 22 | return Promise.resolve(); 23 | } 24 | } 25 | 26 | export { AdminPapersQueue, PCPapersQueue }; 27 | -------------------------------------------------------------------------------- /src/server/routes/api/element-association/grounding-search.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | import { GROUNDING_SEARCH_BASE_URL } from '../../../../config'; 4 | import logger from '../../../logger'; 5 | 6 | const query = ( opts, endpt ) => { 7 | return fetch( GROUNDING_SEARCH_BASE_URL + `/${endpt}`, { 8 | method: 'POST', 9 | body: JSON.stringify(opts), 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | } 13 | } ) 14 | .then( res => res.json() ); 15 | }; 16 | 17 | const get = opts => { 18 | return query( opts, 'get' ) 19 | .catch( err => { 20 | logger.error(`Aggregate get failed`); 21 | logger.error(err); 22 | 23 | throw err; 24 | } ); 25 | }; 26 | 27 | const searchAll = opts => query( opts, 'search' ); 28 | 29 | const search = opts => { 30 | let { limit, offset, namespace, name, organismCounts } = opts; 31 | 32 | let queryOpts = { q: name }; 33 | 34 | if ( organismCounts ) { 35 | let cmp = ( a, b ) => organismCounts[ b ] - organismCounts[ a ]; 36 | let nonZero = taxonId => organismCounts[taxonId] !== 0; 37 | let organismOrdering = Object.keys( organismCounts ).filter( nonZero ).sort( cmp ); 38 | 39 | queryOpts.organismOrdering = organismOrdering; 40 | } 41 | 42 | if( namespace ){ 43 | queryOpts.namespace = namespace; 44 | } 45 | 46 | return searchAll( queryOpts ) 47 | .then( ents => ents.slice( offset, offset + limit ) ) 48 | .catch( err => { 49 | logger.error(`Aggregate search failed`); 50 | logger.error(err); 51 | 52 | throw err; 53 | } ); 54 | }; 55 | 56 | export { get, search }; 57 | -------------------------------------------------------------------------------- /src/server/routes/api/element-association/index.js: -------------------------------------------------------------------------------- 1 | import * as groundingSearch from './grounding-search'; 2 | import Express from 'express'; 3 | 4 | const jsonifyResult = response => ( result => response.json( result ) ); 5 | const http = Express.Router(); 6 | const provider = groundingSearch; 7 | 8 | http.post('/search', function( req, res ){ 9 | ( 10 | provider.search( req.body ) 11 | .then( jsonifyResult(res) ) 12 | .catch( err => res.status(500).send(err) ) 13 | ); 14 | }); 15 | 16 | http.post('/get', function( req, res ){ 17 | ( 18 | provider.get( req.body ) 19 | .then( jsonifyResult(res) ) 20 | .catch( err => res.status(500).send(err) ) 21 | ); 22 | }); 23 | 24 | export default http; 25 | -------------------------------------------------------------------------------- /src/server/routes/api/element-association/pc.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import querystring from 'querystring'; 3 | import _ from 'lodash'; 4 | import { tryPromise } from '../../../../util'; 5 | 6 | const BASE_URL = 'http://www.pathwaycommons.org/pc2'; 7 | const LIMIT = 10; 8 | const TAXON_PREFIX = 'http://identifiers.org/taxonomy/'; 9 | 10 | const clean = obj => _.omitBy( obj, _.isUndefined ); 11 | 12 | const organismUriToId = uri => uri == null ? null : uri.substr( TAXON_PREFIX.length ); 13 | 14 | const searchQuery = opts => clean({ q: opts.name, organism: opts.organism, format: 'json' }); 15 | 16 | const searchPostprocess = res => { 17 | return res.searchHit.slice( 0, LIMIT ).map( entry => { 18 | return { 19 | type: entry.biopaxClass, 20 | name: entry.name, 21 | organism: organismUriToId( entry.organism[0] ), 22 | namespace: 'pathwaycommons', 23 | id: entry.uri 24 | }; 25 | } ); 26 | }; 27 | 28 | const getQuery = opts => clean({ uri: opts.id, format: 'jsonld' }); 29 | 30 | const getPostprocess = res => { 31 | let entryIsDescr = entry => entry.comment != null && entry['@type'].toLowerCase().indexOf('reference') >= 0; 32 | 33 | return res['@graph'].filter( entryIsDescr ).map( entry => { 34 | return { 35 | namespace: 'pathwaycommons', 36 | id: entry['@id'], 37 | description: entry.comment[ entry.comment.length - 1 ] 38 | }; 39 | } )[0] || {}; 40 | }; 41 | 42 | const request = ( endpt, query ) => { 43 | let addr = BASE_URL + `/${endpt}?` + querystring.stringify( query ); 44 | 45 | return ( 46 | tryPromise( () => fetch( addr ) ) 47 | .then( res => res.json() ) 48 | ); 49 | }; 50 | 51 | export const search = opts => { 52 | return tryPromise( () => request( 'search', searchQuery(opts) ) ).then( searchPostprocess ); 53 | }; 54 | 55 | export const get = opts => { 56 | return tryPromise( () => request( 'get', getQuery(opts) ) ).then( getPostprocess ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/server/routes/api/index.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import ElementAssociation from './element-association'; 3 | import Document from './document'; 4 | import swaggerJSDoc from 'swagger-jsdoc'; 5 | import swaggerUi from 'swagger-ui-express'; 6 | 7 | import { EMAIL_ADDRESS_INFO, BASE_URL } from '../../../config'; 8 | 9 | let http = Express.Router(); 10 | 11 | const swaggerOpts = { 12 | definition: { 13 | openapi: '3.0.0', 14 | info: { 15 | title: 'Biofactoid web services', 16 | contact: { 17 | name: 'Biofactoid', 18 | url: BASE_URL, 19 | email: EMAIL_ADDRESS_INFO 20 | }, 21 | version: `${process.env.npm_package_version}`, 22 | }, 23 | }, 24 | apis: [ 25 | './src/server/routes/api/**/*.js' 26 | ] 27 | }; 28 | 29 | const swaggerDocument = swaggerJSDoc( swaggerOpts ); 30 | 31 | http.use('/', swaggerUi.serve); 32 | http.get('/', swaggerUi.setup( swaggerDocument )); 33 | 34 | http.use('/element-association', ElementAssociation); 35 | http.use('/document', Document); 36 | 37 | export default http; 38 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { generateSitemap, getDocumentJson, blankDocumentJson } from './api/document'; 3 | import { TWITTER_ACCOUNT_NAME, BASE_URL, EMAIL_ADDRESS_INFO, NODE_ENV, GTM_ID } from '../../config'; 4 | 5 | const http = express.Router(); 6 | 7 | // get the app ui but with static share metadata in the html 8 | const getDocumentPage = function(req, res) { 9 | ( getDocumentJson(req.params.id) 10 | .then(document => res.render('index.html.ejs', { 11 | document, 12 | TWITTER_ACCOUNT_NAME, 13 | BASE_URL, 14 | NODE_ENV, 15 | GTM_ID 16 | })) 17 | .catch( () => res.render( '404', { EMAIL_ADDRESS_INFO } ) ) 18 | ); 19 | }; 20 | 21 | const newDocumentPage = function(req, res) { 22 | ( blankDocumentJson() 23 | .then( doc => res.redirect( 302, doc.privateUrl )) 24 | .catch( () => res.render( '404', { EMAIL_ADDRESS_INFO } ) ) 25 | ); 26 | }; 27 | 28 | http.get('/robots.txt', function( req, res ) { 29 | res.set('Content-Type', 'text/plain'); 30 | const text = `User-agent: *\nAllow: /\nDisallow: /document/new\n\nSitemap: ${BASE_URL}/sitemap.xml`; 31 | res.send( text ); 32 | }); 33 | 34 | http.get('/sitemap.xml', function( req, res, next ) { 35 | res.set('Content-Type', 'application/xml'); 36 | generateSitemap() 37 | .then( sitemap => res.send( sitemap ) ) 38 | .catch( next ); 39 | }); 40 | 41 | http.get('/document/new', newDocumentPage); 42 | http.get('/document/:id/:secret', getDocumentPage); 43 | http.get('/document/:id', getDocumentPage); 44 | 45 | // get the app ui 46 | http.get('*', function(req, res) { 47 | res.render('index.html.ejs', { 48 | TWITTER_ACCOUNT_NAME, 49 | BASE_URL, 50 | NODE_ENV, 51 | GTM_ID 52 | }); 53 | }); 54 | 55 | export default http; 56 | -------------------------------------------------------------------------------- /src/server/routes/socket.io.js: -------------------------------------------------------------------------------- 1 | // an object to hold socket.io bindings without a ref to an actual socket.io instance (yet) 2 | 3 | class Io { 4 | static router(){ return new Io(); } 5 | 6 | constructor(){} 7 | 8 | get bindings(){ 9 | let bgs = this._bindings = this._bindings || []; 10 | 11 | return bgs; 12 | } 13 | 14 | on( type, handler ){ 15 | this.bindings.push({ 16 | type: type, 17 | handler: handler 18 | }); 19 | 20 | return this; 21 | } 22 | 23 | of( ns ){ 24 | this.namespace = ns; 25 | 26 | return this; 27 | } 28 | 29 | bind( io ){ 30 | let nsIo; 31 | 32 | if( this.namespace ){ 33 | nsIo = io.of( this.namespace ); 34 | } else { 35 | nsIo = io; 36 | } 37 | 38 | this.bindings.forEach( b => nsIo.on( b.type, b.handler ) ); 39 | 40 | return this; 41 | } 42 | 43 | use( io ){ 44 | this.bind( io ); 45 | 46 | return this; 47 | } 48 | } 49 | 50 | export default Io; 51 | -------------------------------------------------------------------------------- /src/server/routes/style-demo.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | var http = express.Router(); 3 | 4 | http.get('/', function(req, res) { 5 | res.render('style-demo.html'); 6 | }); 7 | 8 | export default http; 9 | -------------------------------------------------------------------------------- /src/server/update-cron.js: -------------------------------------------------------------------------------- 1 | import documentUpdate from './routes/api/document/update'; 2 | import { refreshGraphDB } from './routes/api/document/graphdb'; 3 | 4 | import { 5 | GRAPHDB_CRON_REFRESH_PERIOD_MINUTES 6 | } from '../config'; 7 | 8 | const updateCron = async () => { 9 | await documentUpdate(); 10 | await refreshGraphDB( GRAPHDB_CRON_REFRESH_PERIOD_MINUTES ); 11 | }; 12 | 13 | export default updateCron; -------------------------------------------------------------------------------- /src/styles/accordion.css: -------------------------------------------------------------------------------- 1 | .accordion { 2 | margin: auto; 3 | width: 70%; 4 | padding: 10px; 5 | } 6 | 7 | .accordion-item { 8 | padding: 1em 0.5em; 9 | border-bottom: 1px solid rgb(234, 234, 234); 10 | } 11 | 12 | .accordion-item-header { 13 | position: relative; 14 | } 15 | 16 | .accordion-item-header .accordion-item-header-title { 17 | margin: 0; 18 | max-width: 90%; 19 | } 20 | 21 | .accordion-item-header:hover { 22 | cursor: pointer; 23 | } 24 | 25 | .accordion-item-header-icon { 26 | position: absolute; 27 | top: 50%; 28 | right: 0; 29 | transform: translateY(-50%); 30 | } 31 | 32 | .accordion .accordion-item-content { 33 | padding: 0 1em; 34 | opacity: 0; 35 | max-height: 0; 36 | overflow: hidden; 37 | 38 | & p { 39 | color: #000; 40 | } 41 | 42 | & ul { 43 | margin-top: 0; 44 | } 45 | 46 | & li { 47 | margin: 0; 48 | } 49 | } 50 | 51 | .accordion .accordion-item .accordion-item-header.open { 52 | margin-bottom: 15px; 53 | } 54 | 55 | .accordion .accordion-item .accordion-item-header.open + .accordion-item-content { 56 | max-height: 2000px; 57 | opacity: 1; 58 | } 59 | 60 | @media (max-width: 750px) { 61 | .accordion { 62 | width: 100%; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/styles/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { 3 | transform: rotate(45deg); 4 | } 5 | 6 | 100% { 7 | transform: rotate(404deg); 8 | } 9 | } 10 | 11 | @keyframes fadeIn { 12 | 0% { 13 | opacity: 0; 14 | } 15 | 16 | 100% { 17 | opacity: 1; 18 | } 19 | } 20 | 21 | @keyframes shiftBgRight { 22 | 0% { 23 | background-position-x: 0; 24 | } 25 | 26 | 100% { 27 | background-position-x: 400px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/styles/app.css: -------------------------------------------------------------------------------- 1 | body.app { 2 | overflow: auto; 3 | } 4 | 5 | .video-embed { 6 | box-sizing: border-box; 7 | border: 1px solid #000; 8 | position: relative; 9 | width: 100%; 10 | height: 0; 11 | padding-bottom: 57%; 12 | } 13 | 14 | .video-embed-iframe { 15 | width: 100%; 16 | height: 100%; 17 | position: absolute; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/citation.css: -------------------------------------------------------------------------------- 1 | .citation .citation-title { 2 | font-size: 1.5em; 3 | } 4 | 5 | .citation ul { 6 | margin: 0.25em 0; 7 | padding: 0; 8 | 9 | & li { 10 | display: inline-block; 11 | list-style-type: none; 12 | padding: 0; 13 | margin: 0; 14 | 15 | & i { 16 | margin-left: 0.15em; 17 | } 18 | } 19 | 20 | & li::after { 21 | content: '\00b7'; 22 | margin: 0 0.5em; 23 | } 24 | li:last-child::after { content: none; } 25 | } 26 | 27 | .citation .citation-flags { 28 | margin: 0.5em 0; 29 | 30 | & .citation-flag { 31 | padding: 0.2em 0.5em; 32 | 33 | &.danger { 34 | color: #000; 35 | background-color: rgba(255, 0, 0, 0.3); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/copy-field.css: -------------------------------------------------------------------------------- 1 | .copy-field { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .copy-field-input { 7 | flex-grow: 1; 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/custom-icons.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | &.icon-spinner { 3 | transform: translate3d(0, 0, 0); 4 | background-image: url('./image/spinner.svg'); 5 | animation: spin 750ms infinite linear; 6 | } 7 | 8 | &.icon-logo { 9 | background-image: url('./image/logo.svg'); 10 | } 11 | 12 | &.icon-t { 13 | background-image: url('./image/icon-twitter.svg'); 14 | } 15 | 16 | &.icon-t-white { 17 | background-image: url('./image/icon-twitter-white.svg'); 18 | } 19 | 20 | &.icon-arrow-positive { 21 | background-image: url('./image/arrow-positive-long-filled.svg'); 22 | 23 | &.icon-white { 24 | background-image: url('./image/arrow-positive-long-filled-white.svg'); 25 | } 26 | } 27 | 28 | &.icon-arrow-negative { 29 | background-image: url('./image/arrow-negative-small.svg'); 30 | 31 | &.icon-white { 32 | background-image: url('./image/arrow-negative-small-white.svg'); 33 | } 34 | } 35 | 36 | &.icon-arrow-unsigned { 37 | background-image: url('./image/arrow-unsigned.svg'); 38 | 39 | &.icon-white { 40 | background-image: url('./image/arrow-unsigned-white.svg'); 41 | } 42 | } 43 | 44 | &.icon-shr { 45 | background-image: url('./image/share.svg'); 46 | 47 | &.icon-white { 48 | background-image: url('./image/share-white.svg'); 49 | } 50 | } 51 | 52 | &.icon-orcid { 53 | background-image: url('./image/orcid-icon.svg'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/deps.css: -------------------------------------------------------------------------------- 1 | @import "normalize.css"; 2 | @import "./vars.css"; 3 | 4 | /* fonts */ 5 | @import "./vendor/open-sans-latin-greek.css"; 6 | @import "./vendor/inconsolata-ascii.css"; 7 | @import "./vendor/material-icons.css"; 8 | @import "./vendor/material-form.css"; 9 | @import "./vendor/bio-icons.css"; 10 | @import "tippy.js/dist/tippy.css"; 11 | -------------------------------------------------------------------------------- /src/styles/document-linkout.css: -------------------------------------------------------------------------------- 1 | .document-linkout-address-val { 2 | margin: 1em 0; 3 | 4 | & .copy-field-input { 5 | width: calc(100% - 2em); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/document-preview.css: -------------------------------------------------------------------------------- 1 | .document-preview-email { 2 | height: 20em; 3 | border: 1px solid #ccc; 4 | border-radius: 0.25em; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/document-seeder.css: -------------------------------------------------------------------------------- 1 | .document-seeder { 2 | & input, 3 | & textarea { 4 | width: 100%; 5 | margin-bottom: 1em; 6 | } 7 | 8 | & textarea { 9 | min-height: 6em; 10 | resize: vertical; 11 | } 12 | } 13 | 14 | .document-seeder-content { 15 | width: 100%; 16 | } 17 | 18 | .document-seeder-text-label { 19 | font-weight: bold; 20 | font-size: var(--smallFontSize); 21 | cursor: default; 22 | } 23 | 24 | .document-seeder-submit { 25 | margin-right: 0.5em; 26 | } 27 | 28 | .document-seeder-submit-spinner { 29 | vertical-align: middle; 30 | } 31 | 32 | .document-seeder-example-buttons { 33 | & button + button { 34 | margin-left: 0.5em; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/editor/cxtmenu.css: -------------------------------------------------------------------------------- 1 | .cxtmenu-command-icon { 2 | font-size: 1.5em; 3 | } 4 | 5 | .cxtmenu { 6 | user-select: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/editor/index.css: -------------------------------------------------------------------------------- 1 | @import "./editor.css"; 2 | @import "./cxtmenu.css"; 3 | @import "./undo-remove.css"; 4 | -------------------------------------------------------------------------------- /src/styles/editor/undo-remove.css: -------------------------------------------------------------------------------- 1 | .editor-undo-rm { 2 | position: absolute; 3 | top: 4em; 4 | left: 50%; 5 | transform: translate(-50%, 0); 6 | text-align: center; 7 | z-index: 2; 8 | border-radius: 0.25em; 9 | box-shadow: 0 4px 20px 4px rgba(0, 20, 60, 0.2), 0 4px 80px -8px rgba(0, 20, 60, 0.3); 10 | margin-top: 0.5em; 11 | opacity: 1; 12 | transition-property: opacity; 13 | transition-duration: 500ms; 14 | display: none !important; 15 | } 16 | 17 | .editor-undo-rm-unavailable { 18 | opacity: 0; 19 | pointer-events: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/element-info/element-info.css: -------------------------------------------------------------------------------- 1 | .element-info { 2 | } 3 | 4 | .element-info-message { 5 | text-align: center; 6 | } 7 | 8 | .element-info-no-data { 9 | color: var(--widgetDisabledTextColor); 10 | } 11 | 12 | .element-info-progression { 13 | margin-top: 1em; 14 | display: flex; 15 | flex-direction: row; 16 | } 17 | 18 | .element-info-back-area, 19 | .element-info-forward-area { 20 | display: flex; 21 | flex-direction: row; 22 | flex-grow: 1; 23 | } 24 | 25 | .element-info-progression { 26 | & .popover-target { 27 | display: flex; 28 | flex-direction: row; 29 | flex-grow: 1; 30 | } 31 | } 32 | 33 | .element-info-back, 34 | .element-info-forward { 35 | font-size: 1.25em; 36 | flex-grow: 1; 37 | } 38 | 39 | .element-info-progression-button-label { 40 | display: flex; 41 | justify-content: center; 42 | flex-grow: 1; 43 | } 44 | 45 | .element-info-complete-icon { 46 | color: var(--completeColor); 47 | } 48 | 49 | .element-info-edit { 50 | margin-left: 0.25em; 51 | vertical-align: baseline; 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/element-info/index.css: -------------------------------------------------------------------------------- 1 | @import "./element-info.css"; 2 | @import "./entity-info.css"; 3 | @import "./interaction-info.css"; 4 | @import "./participant-info.css"; 5 | 6 | .editor-info-panel .interaction-info-summary-text, 7 | .editor-info-panel .entity-info-name { 8 | font-size: 1.5em; 9 | font-weight: normal; 10 | margin-top: 0; 11 | margin-right: 1.125em; 12 | } 13 | 14 | .editor-info-panel .interaction-info, 15 | .editor-info-panel .entity-info { 16 | width: auto; 17 | max-width: 100%; 18 | max-height: unset; 19 | padding: 0; 20 | overflow: visible; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/element-info/interaction-info.css: -------------------------------------------------------------------------------- 1 | .interaction-info { 2 | position: relative; 3 | box-sizing: border-box; 4 | padding: 0.25em; 5 | min-width: 300px; 6 | max-width: calc(100vw - 1.5em); 7 | width: 400px; 8 | max-height: 80vh; 9 | overflow: scroll; 10 | } 11 | 12 | .interaction-info-ro { 13 | width: 100vw; 14 | } 15 | 16 | .interaction-info-type-select-label { 17 | margin-top: 1em; 18 | } 19 | 20 | .interaction-info-type-select-label-dir { 21 | color: var(--fadedColor); 22 | margin-left: 0.5em; 23 | } 24 | 25 | textarea.interaction-info-description { 26 | width: 100%; 27 | height: 5em; 28 | resize: none; 29 | overflow: scroll; 30 | } 31 | 32 | .interaction-info-description-label, 33 | .interaction-info-assoc-label { 34 | display: block; 35 | font-weight: bold; 36 | font-size: var(--smallFontSize); 37 | 38 | &:not(:first-child) { 39 | margin-top: 0.75em; 40 | } 41 | } 42 | 43 | .interaction-info-type-radio-indented { 44 | margin-left: 1.25em; 45 | } 46 | 47 | .interaction-info-assoc-radio-label { 48 | display: block; 49 | } 50 | 51 | .interaction-info-assoc-radioset-label { 52 | font-size: var(--smallFontSize); 53 | font-weight: bold; 54 | } 55 | 56 | .interaction-info-notification { 57 | margin-bottom: 0.5em; 58 | } 59 | 60 | .interaction-info-summary-text { 61 | font-weight: bold; 62 | } 63 | 64 | .interaction-info-edit { 65 | text-align: center; 66 | margin-top: 1em; 67 | 68 | & button { 69 | display: inline-block; 70 | height: auto; 71 | } 72 | } 73 | 74 | .interaction-info-carousel-title { 75 | font-weight: bold; 76 | margin-top: 1em; 77 | } 78 | 79 | .interaction-info-carousel { 80 | margin-left: -0.25em; 81 | margin-right: -0.25em; 82 | 83 | & .carousel-doc { 84 | border-color: #ccc; 85 | } 86 | 87 | & .carousel-pager { 88 | background-color: rgba(255, 255, 255, 0.7); 89 | color: #000; 90 | } 91 | } 92 | 93 | .interaction-info-reld-papers-title { 94 | font-weight: bold; 95 | margin-top: 1.5em; 96 | margin-bottom: 0.5em; 97 | } 98 | -------------------------------------------------------------------------------- /src/styles/element-info/participant-info.css: -------------------------------------------------------------------------------- 1 | .participant-info { 2 | position: relative; 3 | padding: 0.25em; 4 | } 5 | 6 | .participant-info-type-select { 7 | width: 100%; 8 | } 9 | 10 | .participant-info-type-select-label { 11 | display: block; 12 | font-weight: bold; 13 | font-size: var(--smallFontSize); 14 | } 15 | 16 | .participant-info-type-select, 17 | .participant-info-type-text { 18 | font-size: var(--smallFontSize); 19 | } 20 | 21 | .participant-info-type-text { 22 | margin-top: 0.333em; 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/form-editor/index.css: -------------------------------------------------------------------------------- 1 | @import "./form-editor.css"; -------------------------------------------------------------------------------- /src/styles/highlighter.css: -------------------------------------------------------------------------------- 1 | .highlighter-term { 2 | background-color: rgba(255, 255, 0, 0.2); 3 | } 4 | 5 | .highlighter { 6 | display: inline-block; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-negative-small-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-negative-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-negative.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-positive-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-positive-long-filled-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-positive-long-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-positive-long.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-positive-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-positive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/image/arrow-unsigned-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/arrow-unsigned.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/banner-puzzle-missing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/banner-puzzle-missing.jpg -------------------------------------------------------------------------------- /src/styles/image/banner-puzzle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/banner-puzzle.jpg -------------------------------------------------------------------------------- /src/styles/image/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/banner.jpg -------------------------------------------------------------------------------- /src/styles/image/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/banner.png -------------------------------------------------------------------------------- /src/styles/image/card-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/card-banner.png -------------------------------------------------------------------------------- /src/styles/image/example-doc-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/example-doc-1.png -------------------------------------------------------------------------------- /src/styles/image/example-doc-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/example-doc-2.png -------------------------------------------------------------------------------- /src/styles/image/fa-angle-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/styles/image/home-figure-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/home-figure-1.jpg -------------------------------------------------------------------------------- /src/styles/image/home-figure-2-slim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/home-figure-2-slim.png -------------------------------------------------------------------------------- /src/styles/image/home-figure-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/home-figure-2.png -------------------------------------------------------------------------------- /src/styles/image/home-figure-fg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/home-figure-fg-2.png -------------------------------------------------------------------------------- /src/styles/image/home-figure-old-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/home-figure-old-2.jpg -------------------------------------------------------------------------------- /src/styles/image/home-figure-old-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/home-figure-old-3.jpg -------------------------------------------------------------------------------- /src/styles/image/ic_data_usage_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/styles/image/ic_keyboard_tab_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/styles/image/ic_person_pin_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/styles/image/icon-twitter-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/image/icon-twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/image/layer-cake-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/layer-cake-2.png -------------------------------------------------------------------------------- /src/styles/image/logo-no-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/styles/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/logo.png -------------------------------------------------------------------------------- /src/styles/image/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/styles/image/logo_harvardu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/logo_harvardu.png -------------------------------------------------------------------------------- /src/styles/image/logo_ohsu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/logo_ohsu.jpg -------------------------------------------------------------------------------- /src/styles/image/logo_utoronto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/logo_utoronto.png -------------------------------------------------------------------------------- /src/styles/image/macbookgrey_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/macbookgrey_front.png -------------------------------------------------------------------------------- /src/styles/image/macbookpro15_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/macbookpro15_front.png -------------------------------------------------------------------------------- /src/styles/image/orcid-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /src/styles/image/phone-explore-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/phone-explore-1.png -------------------------------------------------------------------------------- /src/styles/image/phone-explore-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/phone-explore-2.png -------------------------------------------------------------------------------- /src/styles/image/phone-explore-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/phone-explore-3.png -------------------------------------------------------------------------------- /src/styles/image/sample-editor-screen-fade-to-white.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/sample-editor-screen-fade-to-white.mp4 -------------------------------------------------------------------------------- /src/styles/image/sample-editor-screen-fade.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/sample-editor-screen-fade.mp4 -------------------------------------------------------------------------------- /src/styles/image/sample-editor-screen-ras-raf.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/sample-editor-screen-ras-raf.mp4 -------------------------------------------------------------------------------- /src/styles/image/sample-editor-screen.m4v: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/image/sample-editor-screen.m4v -------------------------------------------------------------------------------- /src/styles/image/share-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/image/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import "./vars.css"; 2 | 3 | /* app styles */ 4 | @import "./animations.css"; 5 | @import "./base.css"; 6 | @import "./custom-icons.css"; 7 | @import "./popover"; 8 | @import "./carousel.css"; 9 | @import "./accordion.css"; 10 | @import "./related-papers.css"; 11 | @import "./react-tabs.css"; 12 | @import "./init-app.css"; 13 | @import "./app.css"; 14 | @import "./highlighter.css"; 15 | @import "./editor"; 16 | @import "./citation.css"; 17 | @import "./help"; 18 | @import "./home.css"; 19 | @import "./request-form.css"; 20 | @import "./document-linkout.css"; 21 | @import "./notification.css"; 22 | @import "./element-info"; 23 | @import "./document-seeder.css"; 24 | @import "./document-management.css"; 25 | @import "./tasks.css"; 26 | @import "./main-menu.css"; 27 | @import "./copy-field.css"; 28 | @import "./twitter-share.css"; 29 | @import "./native-share.css"; 30 | -------------------------------------------------------------------------------- /src/styles/init-app.css: -------------------------------------------------------------------------------- 1 | .init-app { 2 | position: fixed; 3 | z-index: 999999999; 4 | left: 0; 5 | top: 0; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | width: 100%; 10 | height: 100%; 11 | background: #fff; 12 | } 13 | 14 | .init-app-icon { 15 | font-size: 2em; 16 | } 17 | 18 | .init-app-initted { 19 | transition-property: opacity; 20 | transition-duration: 250ms; 21 | transition-timing-function: ease-out; 22 | pointer-events: none; 23 | opacity: 0; 24 | 25 | & .icon { 26 | animation: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/main-menu.css: -------------------------------------------------------------------------------- 1 | .main-menu { 2 | z-index: 1; 3 | display: inline-flex; 4 | flex-direction: row; 5 | align-items: center; 6 | } 7 | 8 | .main-menu-button { 9 | margin: 0.25em; 10 | width: 1.75em; 11 | height: 1.75em; 12 | font-size: 1.25em; 13 | } 14 | 15 | .main-menu-set { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: flex-start; 19 | padding: 0.125em; 20 | 21 | & + * { 22 | border-top: 1px solid #ccc; 23 | } 24 | } 25 | 26 | .main-menu-item { 27 | width: 100%; 28 | text-align: left; 29 | } 30 | 31 | .main-menu-linkouts { 32 | min-width: 14em; 33 | max-width: 40em; 34 | width: 40vw; 35 | padding: 0.25em; 36 | 37 | & .document-linkout-address-name:first-child { 38 | margin-top: 0; 39 | } 40 | 41 | & .document-linkout-address:last-child .document-linkout-address-val { 42 | margin-bottom: 0; 43 | } 44 | } 45 | 46 | .main-menu-logo { 47 | font-size: 2.25em; 48 | margin-right: 0.125em; 49 | transition-property: filter; 50 | transition-timing-function: ease-out; 51 | transition-duration: 250ms; 52 | cursor: pointer; 53 | display: flex; 54 | flex-direction: row; 55 | align-items: center; 56 | 57 | &:active { 58 | filter: brightness(50%); 59 | } 60 | } 61 | 62 | .icon-logo-beside { 63 | font-size: 0.6em; 64 | } 65 | 66 | .main-menu-title { 67 | margin-left: 0.5em; 68 | } 69 | -------------------------------------------------------------------------------- /src/styles/native-share.css: -------------------------------------------------------------------------------- 1 | .native-share { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/popover/index.css: -------------------------------------------------------------------------------- 1 | @import "./tippy.css"; 2 | @import "./popover.css"; 3 | @import "./tooltip.css"; 4 | -------------------------------------------------------------------------------- /src/styles/popover/popover.css: -------------------------------------------------------------------------------- 1 | .popover-content { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/popover/tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip-content { 2 | padding: 0.333em; 3 | display: flex; 4 | flex-direction: row; 5 | align-items: baseline; 6 | } 7 | 8 | .tooltip-shortcut { 9 | display: inline-block; 10 | border: 1px solid #fff; 11 | background: #fff; 12 | color: #000; 13 | border-radius: 0.25em; 14 | min-width: 1em; 15 | padding: 0 0.1em; 16 | text-transform: uppercase; 17 | text-align: center; 18 | } 19 | 20 | .tooltip-shortcut-label { 21 | font-size: var(--smallFontSize); 22 | display: inline-block; 23 | margin: 0 0.5em; 24 | border-left: 1px solid #888; 25 | padding: 0.0625em; 26 | padding-left: 0.5em; 27 | text-transform: uppercase; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/react-tabs.css: -------------------------------------------------------------------------------- 1 | .react-tabs__tab-list { 2 | list-style: none; 3 | display: flex; 4 | flex-direction: row; 5 | margin: 0; 6 | padding: 0; 7 | margin-bottom: 0.25em; 8 | line-height: inherit; 9 | } 10 | 11 | .react-tabs__tab { 12 | display: flex; 13 | position: relative; 14 | flex-grow: 1; 15 | margin: 0; 16 | padding: 0 0.25em; 17 | border-bottom: 2px solid #ccc; 18 | color: #888; 19 | cursor: pointer; 20 | justify-content: center; 21 | transition-property: border-bottom-color, color; 22 | transition-duration: 500ms; 23 | transition-timing-function: linear; 24 | text-align: center; 25 | } 26 | 27 | .react-tabs__tab--selected, 28 | .react-tabs__tab:active { 29 | border-bottom-color: #000; 30 | color: #000; 31 | } 32 | 33 | /* re-enable stylelint on these blocks if they used eventually... */ 34 | .react-tabs__tab--selected {} /* stylelint-disable-line */ 35 | .react-tabs__tab-panel {} /* stylelint-disable-line */ 36 | -------------------------------------------------------------------------------- /src/styles/related-papers.css: -------------------------------------------------------------------------------- 1 | .related-papers { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr 1fr; 4 | column-gap: 0.5em; 5 | row-gap: 0.5em; 6 | } 7 | 8 | @media (max-width: 1000px) { 9 | .related-papers { 10 | grid-template-columns: 1fr 1fr; 11 | } 12 | } 13 | 14 | @media (max-width: 500px) { 15 | .related-papers { 16 | grid-template-columns: 1fr; 17 | } 18 | } 19 | 20 | .related-paper { 21 | position: relative; 22 | box-sizing: border-box; 23 | border: 1px solid #bbb; 24 | overflow: hidden; 25 | height: 20em; 26 | border-radius: 0.25em; 27 | padding: 0.5em; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-between; 31 | min-width: 15em; 32 | } 33 | 34 | .related-paper-title { 35 | line-height: 1.4; 36 | } 37 | 38 | .related-paper-abstract { 39 | position: relative; 40 | overflow: hidden; 41 | margin: 0.5em 0; 42 | flex: 1 1; 43 | font-size: 0.8em; 44 | line-height: 1.4; 45 | 46 | &::after { 47 | content: ''; 48 | position: absolute; 49 | bottom: 0; 50 | height: 3em; 51 | left: 0; 52 | right: 0; 53 | background: linear-gradient(to top, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%); 54 | } 55 | } 56 | 57 | .related-paper-author { 58 | font-style: italic; 59 | margin-bottom: 0.5em; 60 | } 61 | 62 | .related-paper-journal { 63 | font-style: italic; 64 | } 65 | 66 | .related-papers-empty { 67 | display: block; 68 | background: #f0f0f0; 69 | border: 1px solid #ddd; 70 | border-radius: 0.25em; 71 | } 72 | 73 | .related-papers-empty-icon { 74 | font-size: 2em; 75 | text-align: center; 76 | margin: 0.5em 0; 77 | opacity: 0.333; 78 | } 79 | 80 | .related-papers-empty-msg { 81 | margin: 1em 0; 82 | box-sizing: border-box; 83 | padding: 0 1em; 84 | color: #888; 85 | text-align: center; 86 | } 87 | -------------------------------------------------------------------------------- /src/styles/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --widgetBorderColor: #aaa; 3 | --widgetBorderRadius: 4px; 4 | --widgetDisabledBorderColor: #ddd; 5 | --widgetDisabledTextColor: #bbb; 6 | --widgetActiveColor: #aaa; 7 | --widgetToggleColor: #ccc; 8 | --buttonActiveColor: #000; 9 | --widgetTransitionDuration: 500ms; 10 | --widgetHeight: 1.75em; 11 | --defaultWidgetWidth: 12em; 12 | --activeColor: #3996db; 13 | --activeColorDark: #b8e0ff; /* for dark bg */ 14 | --smallFontSize: 0.8em; 15 | --fadedColor: #888; 16 | --invalidColor: #b53535; 17 | --completeColor: #38af58; 18 | --brandColor: #fabb2d; 19 | --brandColorDark: #cc9b2a; 20 | --brandColorSuperDark: #af8728; 21 | --brandColorLight: #fac751; 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/vendor/bio-icons.css: -------------------------------------------------------------------------------- 1 | /* 2 | Icon Font: bio-icons 3 | */ 4 | 5 | @font-face { 6 | font-family: "bio-icons"; 7 | src: url("./fonts/bio-icons/bio-icons.eot"); 8 | src: url("./fonts/bio-icons/bio-icons.woff") format("woff"); 9 | /*src: url("./fonts/bio-icons/bio-icons.eot?#iefix") format("embedded-opentype"), 10 | url("./fonts/bio-icons/bio-icons.woff") format("woff"), 11 | url("./fonts/bio-icons/bio-icons.ttf") format("truetype"), 12 | url("./fonts/bio-icons/bio-icons.svg#bio-icons") format("svg");*/ 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /*@media screen and (-webkit-min-device-pixel-ratio:0) { 18 | @font-face { 19 | font-family: "bio-icons"; 20 | src: url("./fonts/bio-icons/bio-icons.svg#bio-icons") format("svg"); 21 | } 22 | }*/ 23 | 24 | [data-icon]:before { content: attr(data-icon); } 25 | 26 | [data-icon]:before, 27 | .bio-cells:before, 28 | .bio-fish:before, 29 | .bio-fly:before, 30 | .bio-helix:before, 31 | .bio-human:before, 32 | .bio-mouse:before, 33 | .bio-plant:before, 34 | .bio-rat:before, 35 | .bio-worm:before, 36 | .bio-yeast:before, 37 | .bio-yeasts:before { 38 | display: inline-block; 39 | font-family: "bio-icons"; 40 | font-style: normal; 41 | font-weight: normal; 42 | font-variant: normal; 43 | line-height: 1; 44 | text-decoration: inherit; 45 | text-rendering: optimizeLegibility; 46 | text-transform: none; 47 | -moz-osx-font-smoothing: grayscale; 48 | -webkit-font-smoothing: antialiased; 49 | font-smoothing: antialiased; 50 | } 51 | 52 | .bio-cells:before { content: "\f100"; } 53 | .bio-fish:before { content: "\f101"; } 54 | .bio-fly:before { content: "\f102"; } 55 | .bio-helix:before { content: "\f103"; } 56 | .bio-human:before { content: "\f104"; } 57 | .bio-mouse:before { content: "\f105"; } 58 | .bio-plant:before { content: "\f106"; } 59 | .bio-rat:before { content: "\f107"; } 60 | .bio-worm:before { content: "\f108"; } 61 | .bio-yeast:before { content: "\f10a"; } 62 | .bio-yeasts:before { content: "\f109"; } 63 | -------------------------------------------------------------------------------- /src/styles/vendor/font-awesome-custom.css: -------------------------------------------------------------------------------- 1 | /* custom classes to use with fa */ 2 | 3 | /* normalises fa with md icon size */ 4 | .fa-md { 5 | font-size: 0.8em; 6 | vertical-align: 0.08em; 7 | display: inline-block; 8 | width: calc(1/0.8em); 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/vendor/font-awesome-form.css: -------------------------------------------------------------------------------- 1 | input[type="radio"], 2 | input[type="checkbox"] { 3 | position: absolute; 4 | opacity: 0.00001; 5 | pointer-events: none; 6 | } 7 | 8 | input[type="radio"] + label::before, 9 | input[type="checkbox"] + label::before { 10 | display: inline-block; 11 | font: normal normal normal 14px/1 FontAwesome; 12 | font-size: inherit; 13 | text-rendering: auto; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | width: 1.28571429em; 17 | text-align: center; 18 | color: var(--widgetBorderColor); 19 | cursor: pointer; 20 | } 21 | 22 | input + label { 23 | cursor: pointer; 24 | } 25 | 26 | input[type="checkbox"] + label::before { 27 | content: '\f096'; 28 | } 29 | 30 | input[type="checkbox"]:checked + label::before { 31 | /*content: '\f14a';*/ 32 | content: '\f046'; 33 | position: relative; 34 | left: 0.05em; 35 | color: var(--widgetActiveColor); 36 | } 37 | 38 | input[type="radio"] + label::before { 39 | content: '\f1db'; 40 | } 41 | 42 | input[type="radio"]:checked + label::before { 43 | content: '\f192'; 44 | color: var(--widgetActiveColor); 45 | } 46 | -------------------------------------------------------------------------------- /src/styles/vendor/fonts/bio-icons/bio-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/bio-icons/bio-icons.eot -------------------------------------------------------------------------------- /src/styles/vendor/fonts/bio-icons/bio-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/bio-icons/bio-icons.ttf -------------------------------------------------------------------------------- /src/styles/vendor/fonts/bio-icons/bio-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/bio-icons/bio-icons.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/font-awesome/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/font-awesome/FontAwesome.otf -------------------------------------------------------------------------------- /src/styles/vendor/fonts/font-awesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/font-awesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/styles/vendor/fonts/font-awesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/font-awesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/styles/vendor/fonts/font-awesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/font-awesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/font-awesome/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/font-awesome/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/inconsolata-ascii/inconsolata-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/inconsolata-ascii/inconsolata-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/inconsolata-ascii/inconsolata-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/inconsolata-ascii/inconsolata-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/inconsolata/generator_config.txt: -------------------------------------------------------------------------------- 1 | # Font Squirrel Font-face Generator Configuration File 2 | # Upload this file to the generator to recreate the settings 3 | # you used to create these fonts. 4 | 5 | {"mode":"expert","formats":["woff","woff2"],"tt_instructor":"keep","fix_gasp":"xy","fix_vertical_metrics":"Y","metrics_ascent":"","metrics_descent":"","metrics_linegap":"","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"none","subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","style_link":"Y","css_stylesheet":"inconsolata.css","filename_suffix":"","spacing_adjustment":"0","rememberme":"Y"} -------------------------------------------------------------------------------- /src/styles/vendor/fonts/inconsolata/inconsolata.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/inconsolata/inconsolata.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/inconsolata/inconsolata.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/inconsolata/inconsolata.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/material-icons/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/material-icons/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/material-icons/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/material-icons/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/generator_config.txt: -------------------------------------------------------------------------------- 1 | # Font Squirrel Font-face Generator Configuration File 2 | # Upload this file to the generator to recreate the settings 3 | # you used to create these fonts. 4 | 5 | {"mode":"expert","formats":["woff","woff2"],"tt_instructor":"keep","fix_gasp":"xy","fix_vertical_metrics":"N","metrics_ascent":"","metrics_descent":"","metrics_linegap":"","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"advanced","subset_range":["macroman","lowercase","uppercase","numbers","punctuation","currency","typographics","math","altpunctuation","accentedlower","accentedupper","diacriticals","english","greek","ubasic","ulatin1","ucurrency","upunctuation","ulatina","ulatinb","ulatinaddl"],"subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","css_stylesheet":"stylesheet.css","filename_suffix":"-webfont","spacing_adjustment":"0","rememberme":"Y"} -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-bold-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-bold-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-bolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-bolditalic-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-bolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-bolditalic-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabold-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabold-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabolditalic-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-extrabolditalic-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-italic-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-italic-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-light-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-light-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-lightitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-lightitalic-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-lightitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-lightitalic-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-regular-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibold-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibold-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibolditalic-webfont.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans-latin-greek/opensans-semibolditalic-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/generator_config.txt: -------------------------------------------------------------------------------- 1 | # Font Squirrel Font-face Generator Configuration File 2 | # Upload this file to the generator to recreate the settings 3 | # you used to create these fonts. 4 | 5 | {"mode":"expert","formats":["woff","woff2"],"tt_instructor":"keep","fix_gasp":"xy","fix_vertical_metrics":"Y","metrics_ascent":"","metrics_descent":"","metrics_linegap":"","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"none","subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","style_link":"Y","css_stylesheet":"open-sans.css","filename_suffix":"","spacing_adjustment":"0","rememberme":"Y"} -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-bold.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-bold.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-bolditalic.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-bolditalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-bolditalic.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-italic.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-italic.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-light.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-light.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-lightitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-lightitalic.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-lightitalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-lightitalic.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-regular.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-regular.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-semibold.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-semibold.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-semibolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-semibolditalic.woff -------------------------------------------------------------------------------- /src/styles/vendor/fonts/open-sans/opensans-semibolditalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathwayCommons/factoid/df57bc6ccd1727e6b358a32e95dc388e5a0f13a3/src/styles/vendor/fonts/open-sans/opensans-semibolditalic.woff2 -------------------------------------------------------------------------------- /src/styles/vendor/inconsolata-ascii.css: -------------------------------------------------------------------------------- 1 | /* Generated by Font Squirrel (https://www.fontsquirrel.com) */ 2 | 3 | @font-face { 4 | font-family: 'Inconsolata'; 5 | src: 6 | url('./fonts/inconsolata-ascii/inconsolata-webfont.woff2') format('woff2') 7 | /* url('./fonts/inconsolata-ascii/inconsolata-webfont.woff') format('woff') */ 8 | ; 9 | font-weight: normal; 10 | font-style: normal; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/vendor/inconsolata.css: -------------------------------------------------------------------------------- 1 | /* Generated by Font Squirrel (https://www.fontsquirrel.com) */ 2 | 3 | @font-face { 4 | font-family: 'Inconsolata'; 5 | src: 6 | url('./fonts/inconsolata/inconsolata.woff2') format('woff2') 7 | /* url('./fonts/inconsolata/inconsolata.woff') format('woff') */ 8 | ; 9 | font-weight: normal; 10 | font-style: normal; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/vendor/material-form.css: -------------------------------------------------------------------------------- 1 | input[type="radio"], 2 | input[type="checkbox"] { 3 | position: absolute; 4 | opacity: 0.00001; 5 | pointer-events: none; 6 | } 7 | 8 | input[type="radio"] + label:before, 9 | input[type="checkbox"] + label::before { 10 | font: normal normal normal 24px/1 'Material Icons'; 11 | font-size: inherit; 12 | text-rendering: auto; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | line-height: inherit; 16 | vertical-align: bottom; 17 | transition-property: color; 18 | transition-duration: 250ms; 19 | transition-timing-function: ease-in-out; 20 | 21 | /* Support for all WebKit browsers. */ 22 | -webkit-font-smoothing: antialiased; 23 | 24 | /* Support for Safari and Chrome. */ 25 | text-rendering: optimizeLegibility; 26 | 27 | /* Support for Firefox. */ 28 | -moz-osx-font-smoothing: grayscale; 29 | 30 | /* Support for IE. */ 31 | font-feature-settings: 'liga'; 32 | 33 | display: inline-block; 34 | width: 1.28571429em; 35 | text-align: center; 36 | color: var(--widgetBorderColor); 37 | 38 | cursor: pointer; 39 | } 40 | 41 | input + label, 42 | input + label { 43 | cursor: pointer; 44 | } 45 | 46 | input[type="checkbox"] + label::before { 47 | content: '\e835'; 48 | } 49 | 50 | input[type="checkbox"]:checked + label::before { 51 | content: '\e834'; 52 | color: inherit; 53 | } 54 | 55 | input[type="radio"] + label::before { 56 | content: '\e836'; 57 | } 58 | 59 | input[type="radio"]:checked + label::before { 60 | content: '\e837'; 61 | color: inherit; 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/vendor/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: 6 | local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url('./fonts/material-icons/MaterialIcons-Regular.woff2') format('woff2') 9 | ; 10 | } 11 | 12 | .material-icons { 13 | font: normal normal normal 24px/1 'Material Icons'; 14 | font-size: inherit; 15 | text-rendering: auto; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | line-height: inherit; 19 | vertical-align: bottom; 20 | 21 | /* Support for all WebKit browsers. */ 22 | -webkit-font-smoothing: antialiased; 23 | 24 | /* Support for Safari and Chrome. */ 25 | text-rendering: optimizeLegibility; 26 | 27 | /* Support for Firefox. */ 28 | -moz-osx-font-smoothing: grayscale; 29 | 30 | /* Support for IE. */ 31 | font-feature-settings: 'liga'; 32 | 33 | display: inline-block; 34 | } 35 | -------------------------------------------------------------------------------- /src/util/article.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import striptags from 'striptags'; 3 | 4 | // Remove surrounding whitespace, periods, quotations 5 | const trimPlus = str => { 6 | return str 7 | .replace(/[\s]+/g, ' ') // runs of whitespace 8 | .replace(/^[\s."']+|[\s."']+$/g, ''); 9 | }; 10 | 11 | const lowerCase = str => str.toLowerCase(); 12 | // https://www.nlm.nih.gov/databases/dtd/medline_characters.html 13 | // https://www.nlm.nih.gov/archive/20110906/databases/dtd/medline_character_database.html 14 | const deburr = str => _.deburr( str ); 15 | 16 | // Remove HTML formatting markup 17 | const unformat = str => striptags( str, [ 18 | '', '', '', '', '', '', '' 19 | ]); 20 | 21 | 22 | // Replace variations with HYPHEN-MINUS U+002D 23 | const normalizeDash = str => { 24 | return str 25 | .replace(/[\u2011|\u2013|\u2014]+/g, '\u002D') // everything else to hyphen-minus 26 | .replace(/[\u002D]+/g, '\u002D') // runs 27 | .replace(/(mir)-(\d+)/i, '$1$2'); // Normalize micro RNA names 28 | }; 29 | 30 | const doesMatch = ( title, other ) => { 31 | return title === other || _.includes( title, other ) || _.includes( other, title ); 32 | }; 33 | 34 | // PubMed appears to transform LEFT (U+2018) and RIGHT (U+2019) SINGLE QUOTATION MARKs to APOSTROPHE (U+0027) 35 | // Crossref does not apoear to do this 36 | const apostrophize = str => str.replace(/[\u2018|\u2019]/g, '\''); 37 | 38 | /** 39 | * Match a title string against a candidate 40 | * 41 | * @param {string} title provided title 42 | * @param {string} other candidate to match against 43 | * @returns {boolean} true if the title matches 44 | */ 45 | function testTitle( title, other ) { 46 | [ title, other ] = [title, other] 47 | .map( trimPlus ) 48 | .map( lowerCase ) 49 | .map( normalizeDash ) 50 | .map( unformat ) 51 | .map( deburr ) 52 | .map( apostrophize ); 53 | return doesMatch( title, other ); 54 | } 55 | 56 | export { 57 | testTitle 58 | }; -------------------------------------------------------------------------------- /src/util/assert.js: -------------------------------------------------------------------------------- 1 | import { error } from './obj'; 2 | 3 | function assert( condition, message ){ 4 | if( !condition ){ 5 | throw( error( message || 'An assertion failed' ) ); 6 | } 7 | } 8 | 9 | function assertDefined( val, name ){ 10 | assert( val != null, `Expected '${name}' to be defined` ); 11 | } 12 | 13 | function assertFieldDefined( obj, field ){ 14 | assertDefined( obj[ field ], field ); 15 | } 16 | 17 | function assertFieldsDefined( obj, fields ){ 18 | fields.forEach( field => assertFieldDefined( obj, field ) ); 19 | } 20 | 21 | function assertOneOfFieldsDefined( obj, fields ){ 22 | assert( 23 | fields.some( f => obj[f] != null ), 24 | 'Expected at least one of ' + fields.map( f => `'${f}'` ).join(', ') + ' to be defined' 25 | ); 26 | } 27 | 28 | export { 29 | assert, 30 | assertDefined, 31 | assertFieldDefined, 32 | assertFieldsDefined, 33 | assertOneOfFieldsDefined 34 | }; 35 | -------------------------------------------------------------------------------- /src/util/cache.js: -------------------------------------------------------------------------------- 1 | const initCache = ( cache, key, initVal ) => { 2 | let cacheEntry = cache.get( key ); 3 | 4 | if( cacheEntry == null ){ 5 | cacheEntry = initVal; 6 | 7 | cache.set( key, cacheEntry ); 8 | } 9 | 10 | return cacheEntry; 11 | }; 12 | 13 | class SingleValueCache { 14 | constructor(){ this.value = null; } 15 | get(){ return this.value; } 16 | set(k, v){ this.value = v; } 17 | } 18 | 19 | export { initCache, SingleValueCache }; 20 | -------------------------------------------------------------------------------- /src/util/fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTPStatusError 3 | * 4 | * Class representing a Fetch Response error 5 | */ 6 | class HTTPStatusError extends Error { 7 | constructor( message, status, statusText, response ) { 8 | super( message ); 9 | this.status = status; 10 | this.statusText = statusText; 11 | this.name = 'HTTPStatusError'; 12 | this.response = response; 13 | } 14 | } 15 | 16 | /** 17 | * checkHTTPStatus 18 | * 19 | * Check that the Fetch Response is ok or throw an error 20 | * 21 | * @param {Object} response the (node-)Fetch [response]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response} 22 | * @returns {Object} response the (node-)Fetch [response]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response} 23 | * @throws {HTTPStatusError} when response.status is > 200 or >= 300 24 | */ 25 | const checkHTTPStatus = response => { 26 | const { statusText, status, ok } = response; 27 | if ( !ok ) { 28 | throw new HTTPStatusError( `${statusText} (${status})`, status, statusText, response ); 29 | } 30 | return response; 31 | }; 32 | 33 | 34 | export { checkHTTPStatus, HTTPStatusError }; -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | export * from './obj'; 2 | export * from './promise'; 3 | export * from './is'; 4 | export * from './assert'; 5 | export * from './cy'; 6 | export * from './memoize'; 7 | export * from './strings'; 8 | export * from './cache'; 9 | export * from './fetch'; 10 | -------------------------------------------------------------------------------- /src/util/is.js: -------------------------------------------------------------------------------- 1 | let typeofObj = typeof {}; 2 | let typeofWin = typeof window; 3 | 4 | function isClient(){ 5 | return typeofWin === typeofObj; 6 | } 7 | 8 | function isServer(){ 9 | return !isClient(); 10 | } 11 | 12 | function isDoi( str ){ 13 | // 99.3% of CrossRef DOIs (https://www.crossref.org/blog/dois-and-matching-regular-expressions/) 14 | const doiRegex = /^10\.\d{4,9}\/[-._;()/:A-Z0-9]+$/i; 15 | return doiRegex.test( str ); 16 | } 17 | 18 | function isDigits( str ){ 19 | const digitsRegex = /^[0-9.]+$/; 20 | return digitsRegex.test( str ); 21 | } 22 | 23 | export { isClient, isServer, isDoi, isDigits }; 24 | -------------------------------------------------------------------------------- /src/util/memoize.js: -------------------------------------------------------------------------------- 1 | import { jsonHash } from './obj'; 2 | 3 | const getStringifiedKey = function(){ 4 | return jsonHash( Array.from(arguments) ); 5 | }; 6 | 7 | const memoize = ( fn, cache, getKey = getStringifiedKey ) => { 8 | let getVal = args => fn.apply( null, args ); 9 | 10 | if( cache === undefined ){ 11 | cache = new Map(); 12 | } 13 | 14 | return function(){ 15 | let args = arguments; 16 | let key = getKey( ...args ); 17 | let val; 18 | 19 | if( cache.has(key) ){ 20 | val = cache.get(key); 21 | } else { 22 | val = getVal( args ); 23 | 24 | cache.set( key, val ); 25 | } 26 | 27 | return val; 28 | }; 29 | }; 30 | 31 | export { memoize }; 32 | -------------------------------------------------------------------------------- /src/util/promise.js: -------------------------------------------------------------------------------- 1 | import { error } from './obj'; 2 | import CancelablePromise from 'p-cancelable'; 3 | 4 | const slice = ( arr, i, j ) => Array.prototype.slice.call( arr, i, j ); 5 | 6 | function passthrough( fn ){ 7 | return function( val ){ 8 | fn( val ); // also pass just in case you want to use the val 9 | 10 | return val; 11 | }; 12 | } 13 | 14 | function promisifyEmit( emitter ){ 15 | return (function(){ 16 | let args = slice( arguments ); 17 | 18 | return new Promise( ( resolve, reject ) => { 19 | args.push( ( err, val ) => { 20 | if( err ){ 21 | reject( error(err) ); 22 | } else { 23 | resolve( val ); 24 | } 25 | } ); 26 | 27 | emitter.emit.apply( emitter, args ); 28 | } ); 29 | }); 30 | } 31 | 32 | function promiseOn( emitter, evt ){ 33 | return new Promise( resolve => emitter.once( evt, resolve ) ); 34 | } 35 | 36 | function delay( duration ){ 37 | return new Promise( resolve => setTimeout( () => resolve(), duration ) ); 38 | } 39 | 40 | function delayPassthrough( duration ){ 41 | return function( val ){ 42 | return delay( duration ).then( () => val ); 43 | }; 44 | } 45 | 46 | function defer() { 47 | var resolve, reject; 48 | var promise = new Promise(function() { 49 | resolve = arguments[0]; 50 | reject = arguments[1]; 51 | }); 52 | return { 53 | resolve: resolve, 54 | reject: reject, 55 | promise: promise 56 | }; 57 | } 58 | 59 | function tryPromise( fn ){ 60 | return Promise.resolve().then(fn); 61 | } 62 | 63 | function makeCancelable( p ){ 64 | return new CancelablePromise( (resolve, reject) => { 65 | p.then(resolve).catch(reject); 66 | } ); 67 | } 68 | 69 | export { passthrough, promisifyEmit, promiseOn, delay, delayPassthrough, defer, tryPromise, makeCancelable }; 70 | -------------------------------------------------------------------------------- /src/util/registry.js: -------------------------------------------------------------------------------- 1 | const COLLECTIONS = Object.freeze({ 2 | PUBMED: { 3 | dbname: 'PubMed', 4 | dbPrefix: 'pubmed', 5 | }, 6 | NCBI_GENE: { 7 | dbName: 'NCBI Gene', 8 | dbPrefix: 'NCBIGene', 9 | }, 10 | /** 11 | * The Taxonomy Database is a curated classification and nomenclature for all of the organisms 12 | * in the public sequence databases. This currently represents about 10% of the described species of life on the planet. 13 | */ 14 | NCBI_TAXONOMY: { 15 | dbname: 'NCBI Taxonomy', 16 | dbPrefix: 'NCBITaxon', 17 | }, 18 | MESH: { 19 | dbName: 'MeSH', 20 | dbPrefix: 'mesh', 21 | }, 22 | CHEBI: { 23 | dbName: 'ChEBI', 24 | dbPrefix: 'CHEBI', 25 | }, 26 | CELLOSAURUS: { 27 | dbName: 'CELLOSAURUS', 28 | dbPrefix: 'cellosaurus', 29 | }, 30 | UNIPROT: { 31 | dbName: 'UniProt Knowledgebase', 32 | dbPrefix: 'uniprot', 33 | }, 34 | }); 35 | 36 | export { COLLECTIONS }; 37 | -------------------------------------------------------------------------------- /src/util/strings.js: -------------------------------------------------------------------------------- 1 | import dice from 'dice-coefficient'; // sorensen dice coeff 2 | 3 | function stringDistanceMetric(a, b){ 4 | return 1 - dice(a, b); 5 | } 6 | 7 | function longestCommonPrefixLength(str1, str2){ 8 | var minL = Math.min(str1.length, str2.length); 9 | let i = 0; 10 | while( i < minL ) { 11 | if ( str1[i] !== str2[i] ) { 12 | break; 13 | } 14 | 15 | i++; 16 | } 17 | 18 | return i; 19 | } 20 | 21 | const truncateString = ( text, maxChars = 150, texOverflow = '...' ) => { 22 | return text.length > maxChars ? `${text.slice( 0, maxChars )}${texOverflow}` : text; 23 | }; 24 | 25 | const stripFinalPeriod = text => { 26 | if( text[text.length - 1] === '.' ){ 27 | text = text.substring(0, text.length - 1); 28 | } 29 | 30 | return text; 31 | }; 32 | 33 | const fromCamelCase = str => { 34 | return str.replace(/([a-z])([A-Z])/g, '$1 $2'); 35 | }; 36 | 37 | const onlyCapitalizeFirst = str => { 38 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); 39 | }; 40 | 41 | export { stringDistanceMetric, longestCommonPrefixLength, truncateString, stripFinalPeriod, fromCamelCase, onlyCapitalizeFirst }; 42 | -------------------------------------------------------------------------------- /src/util/time.js: -------------------------------------------------------------------------------- 1 | var differenceInMilliseconds = require( 'date-fns/differenceInMilliseconds' ); 2 | 3 | // const HOURS_PER_DAY = 24; 4 | // const MINUTES_PER_HOUR = 60; 5 | // const SECONDS_PER_MINUTE = 60; 6 | const MILLISECONDS_PER_SECOND = 1000; 7 | 8 | const toSeconds = ms => ms / MILLISECONDS_PER_SECOND; 9 | 10 | /** Class representing a timer. */ 11 | class Timer { 12 | 13 | /** 14 | * Create a timer 15 | * @param {number} delay - The time in milliseconds 16 | */ 17 | constructor( delay ) { 18 | this.delay = delay; 19 | this.last = new Date(); 20 | } 21 | 22 | /** 23 | * Reset the timer. 24 | * @return {number} The new last date 25 | */ 26 | reset() { 27 | this.last = new Date(); 28 | } 29 | 30 | /** 31 | * Has the delay elapsed (since last)? 32 | * @return {boolean} True if elapsed. 33 | */ 34 | hasElapsed() { 35 | const elapsed = differenceInMilliseconds( Date.now(), this.last ); 36 | return elapsed >= this.delay; 37 | } 38 | } 39 | 40 | 41 | export { Timer, toSeconds }; -------------------------------------------------------------------------------- /src/views/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Not found 12 | 13 | 14 | 15 |

Not found

16 | 17 |

The resource you are trying to access is not available at this URL. Please carefully check your address bar and try again, or return to the app.

18 |

If problems persist, please contact us at <%=EMAIL_ADDRESS_INFO%>.

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Error 12 | 13 | 14 | 15 |

Error

16 | 17 |

An error occurred and the requested resource could not be sent to you. We have been notified of the issue and are working to resolve it.

18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/biopax/biopax.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { expect } from 'chai'; 4 | import nock from 'nock'; 5 | import _ from 'lodash'; 6 | 7 | import { GROUNDING_SEARCH_BASE_URL } from '../../src/config'; 8 | import { mapToUniprotIds } from '../../src/server/routes/api/document'; 9 | 10 | describe('mapToUniprotIds', function(){ 11 | before( () => { 12 | const rawBiopaxTemplate = fs.readFileSync(path.resolve( __dirname, 'sampleTemplate.json' ), 'utf8'); 13 | this.biopaxTemplate = JSON.parse(rawBiopaxTemplate); 14 | 15 | const dbXref = this.dbXref = { 16 | id: '_testid', 17 | db: 'uniprot' 18 | }; 19 | 20 | const mockRes = [ 21 | { 22 | dbXrefs: [ 23 | dbXref 24 | ] 25 | } 26 | ]; 27 | 28 | nock(GROUNDING_SEARCH_BASE_URL) 29 | .post('/map') 30 | .times(Infinity) 31 | .reply(200, mockRes); 32 | }); 33 | 34 | after( () => { 35 | nock.cleanAll(); 36 | } ); 37 | 38 | it( 'updated uniprot ids', () => { 39 | const entityPaths = ['interactions.0.controller', 'interactions.0.source', 'interactions.0.participants.0', 'interactions.0.participants.1']; 40 | entityPaths.forEach( entityPath => { 41 | entityPaths.push(entityPath + '.components.0'); 42 | entityPaths.push(entityPath + '.components.1'); 43 | } ); 44 | 45 | return mapToUniprotIds(this.biopaxTemplate) 46 | .then( updatedTemplate => { 47 | entityPaths.forEach( entityPath => { 48 | const xrefPath = entityPath + '.xref'; 49 | const xref = _.get(updatedTemplate, xrefPath); 50 | if ( !_.isNil( xref ) ) { 51 | expect(xref.id).to.equal(this.dbXref.id); 52 | expect(xref.db).to.equal(this.dbXref.db); 53 | } 54 | } ); 55 | } ); 56 | } ); 57 | }); 58 | -------------------------------------------------------------------------------- /test/biopax/sampleTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactions":[ 3 | { 4 | "type":"Other Interaction", 5 | "participants":[ 6 | { 7 | "type":"protein", 8 | "name":"Ataxin 3", 9 | "xref":{ 10 | "id":"110616", 11 | "db":"NCBI Gene", 12 | "dbPrefix":"ncbigene" 13 | }, 14 | "organism":{ 15 | "id":"10090", 16 | "db":"taxonomy" 17 | } 18 | }, 19 | { 20 | "type":"complex", 21 | "name":"Complex (Rheb:ubiquitin)", 22 | "xref":null, 23 | "organism":null, 24 | "components":[ 25 | { 26 | "type":"protein", 27 | "name":"Rheb", 28 | "xref":{ 29 | "id":"19744", 30 | "db":"NCBI Gene", 31 | "dbPrefix":"ncbigene" 32 | }, 33 | "organism":{ 34 | "id":"10090", 35 | "db":"taxonomy" 36 | } 37 | },{ 38 | "type":"protein", 39 | "name":"ubiquitin", 40 | "xref":{ 41 | "id":"216818", 42 | "db":"NCBI Gene", 43 | "dbPrefix":"ncbigene" 44 | }, 45 | "organism":{ 46 | "id":"10090", 47 | "db":"taxonomy" 48 | } 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | ], 55 | "pathwayName":"Tributyrin ester-impregnated pH strips for confirming neonatal feeding tube placement: a diagnostic test accuracy study.", 56 | "pathwayId":"e22e6084-6505-4725-8792-7727484fec9b", 57 | "publication":{ 58 | "id":"36175118", 59 | "db":"pubmed" 60 | } 61 | } -------------------------------------------------------------------------------- /test/mock/cache.js: -------------------------------------------------------------------------------- 1 | import ElementCache from '../../src/model/element-cache'; 2 | import _ from 'lodash'; 3 | 4 | let factoryErr = function(){ 5 | throw new Error('Missing factory for cache'); 6 | }; 7 | 8 | let sourceErr = function(){ 9 | throw new Error('Missing source for cache'); 10 | }; 11 | 12 | let sourceNop = function(){ 13 | console.warn('Using nop source for mock cache'); 14 | }; 15 | 16 | class MockCache extends ElementCache { 17 | constructor( opts = {} ){ 18 | super( _.assign( { 19 | secret: 'secret', // used in almost all tests 20 | factory: { // needs to be specified manually 21 | make: factoryErr, 22 | load: factoryErr 23 | } 24 | }, opts ) ); 25 | 26 | this.source = { 27 | elements: new Map(), 28 | has: function( id ){ 29 | return this.elements.has( id ); 30 | }, 31 | get: function( id ){ 32 | return this.elements.get( id ); 33 | }, 34 | add: function( ele ){ 35 | this.elements.set( ele.id(), ele ); 36 | 37 | return Promise.resolve('MockCache source.add() promise'); 38 | }, 39 | remove: function( ele ){ 40 | this.elements.delete( _.isString( ele ) ? ele : ele.id() ); 41 | 42 | return Promise.resolve('MockCache source.remove() promise'); 43 | } 44 | }; 45 | } 46 | 47 | has( id ){ return this.source.has(id); } 48 | 49 | get( id ){ return this.source.get(id); } 50 | 51 | add( ele ){ return this.source.add(ele); } 52 | 53 | remove( ele ){ return this.source.remove(ele); } 54 | } 55 | 56 | export default MockCache; 57 | -------------------------------------------------------------------------------- /test/mock/socket.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | let defaults = { 4 | 5 | }; 6 | 7 | class Socket { 8 | constructor( opts ){ 9 | opts = Object.assign( {}, defaults, opts ); 10 | 11 | this.syncher = opts.syncher; 12 | } 13 | 14 | on(){ 15 | return this; 16 | } 17 | 18 | emit( type /* data..., onServer */ ){ 19 | let onServer = arguments[ arguments.length - 1 ]; 20 | 21 | if( !_.isFunction( onServer ) ){ 22 | onServer = _.noop; 23 | } 24 | 25 | if( type === 'load' ){ 26 | let err = null; 27 | 28 | onServer( err, _.omit( this.syncher.get(), this.syncher.privateFields() ) ); 29 | } else { 30 | onServer(); 31 | } 32 | 33 | return this; 34 | } 35 | 36 | } 37 | 38 | export default Socket; 39 | -------------------------------------------------------------------------------- /test/neo4j-test/connect-neo4j.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import neo4j from 'neo4j-driver'; 3 | 4 | import { closeDriver, getDriver, initDriver } from '../../src/neo4j/neo4j-driver.js'; 5 | 6 | describe('Neo4j Initiate Driver', () => { 7 | it('initDriver Should initialize and return a driver', () => { 8 | const driver = initDriver(); 9 | expect(driver).an.instanceof(neo4j.Driver); 10 | }); 11 | 12 | it('getDriver should return the driver', () => { 13 | initDriver(); 14 | const driver = getDriver(); 15 | expect(driver).an.instanceof(neo4j.Driver); 16 | }); 17 | 18 | it('closeDriver should remove driver instance', async () => { 19 | const driver = await closeDriver(); 20 | expect(driver).to.be.undefined; 21 | }); 22 | }); -------------------------------------------------------------------------------- /test/util/conf.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | 3 | Promise.config({ 4 | warnings: true, 5 | longStackTraces: true 6 | }); 7 | 8 | export const defaultTimeout = 5000; 9 | -------------------------------------------------------------------------------- /test/util/socket-io.js: -------------------------------------------------------------------------------- 1 | let serverIo; 2 | import clientIo from 'socket.io-client'; 3 | let clientSockets = []; 4 | 5 | let client = (function( ns ){ 6 | let socket = clientIo('http://localhost:54321/' + ns, { 7 | // unused sockets shouldn't try to reconnect, potentially interfering with other tests 8 | reconnection: false 9 | }); 10 | 11 | clientSockets.push( socket ); 12 | 13 | return socket; 14 | }); 15 | 16 | let server = (function( ns ){ 17 | return serverIo.of('/' + ns); 18 | }); 19 | 20 | let stop = function(){ 21 | clientSockets.forEach( socket => socket.close() ); 22 | 23 | clientSockets = []; 24 | 25 | serverIo.close(); 26 | }; 27 | 28 | let start = function(){ 29 | serverIo = require('socket.io')(54321); 30 | 31 | return serverIo; 32 | }; 33 | 34 | export { client, server, start, stop }; 35 | -------------------------------------------------------------------------------- /test/util/table.js: -------------------------------------------------------------------------------- 1 | import rdb from 'rethinkdb'; 2 | 3 | const DB_NAME = 'factoid_test'; 4 | 5 | class TableUtil { 6 | 7 | constructor( name ){ 8 | this.name = name; 9 | } 10 | 11 | get rethink(){ 12 | return rdb; 13 | } 14 | 15 | connect( done ){ 16 | if( this.conn ){ 17 | done(); 18 | 19 | return; 20 | } 21 | 22 | rdb.connect({ host: 'localhost', port: 28015 }, ( err, connection ) => { 23 | if( err ){ throw err; } 24 | 25 | this.conn = connection; 26 | 27 | done(); 28 | }); 29 | } 30 | 31 | get table(){ 32 | return rdb.db( DB_NAME ).table( this.name ); 33 | } 34 | 35 | clean( done ){ 36 | this.connect( () => { 37 | rdb.dbDrop( DB_NAME ).run( this.conn, ( err, res ) => { 38 | done(); 39 | } ).catch( () => {} ); 40 | } ); 41 | } 42 | 43 | drop( done ){ 44 | rdb.dbDrop( DB_NAME ).run( this.conn, ( err, res ) => { 45 | if( err ){ throw err; } 46 | 47 | this.conn.close(( err ) => { 48 | if( err ){ throw err; } 49 | 50 | done(); 51 | }); 52 | } ); 53 | } 54 | 55 | create( done ){ 56 | let createDb = ( next ) => { 57 | rdb.dbCreate( DB_NAME ).run( this.conn, ( err, res ) => { 58 | if( err ){ throw err; } 59 | 60 | next(); 61 | } ); 62 | }; 63 | 64 | let checkDbExists = fn => { 65 | rdb.db( DB_NAME ).tableList().run( this.conn, ( err, res ) => { 66 | if( err ){ 67 | fn( false ); 68 | } else { 69 | fn( true ); 70 | } 71 | } ); 72 | }; 73 | 74 | let createTable = next => { 75 | rdb.db( DB_NAME ).tableCreate( this.name ).run( this.conn, ( err, res ) => { 76 | if( err ){ throw err; } 77 | 78 | next(); 79 | } ); 80 | }; 81 | 82 | this.connect(() => { 83 | checkDbExists( ( exists ) => { 84 | if( exists ){ 85 | createTable( done ); 86 | } else { 87 | createDb( () => createTable( done ) ); 88 | } 89 | } ); 90 | }); 91 | } 92 | 93 | deleteEntry( id, done ){ 94 | rdb.db( DB_NAME ).table( this.name ).get( id ).delete().run( this.conn, ( err, res ) => { 95 | if( err ){ throw err; } 96 | 97 | done(); 98 | } ); 99 | } 100 | 101 | 102 | } 103 | 104 | export default TableUtil; 105 | -------------------------------------------------------------------------------- /test/util/when.js: -------------------------------------------------------------------------------- 1 | let when = ( emitter, evt ) => new Promise( resolve => emitter.on(evt, resolve) ); 2 | 3 | let whenN = ( emitter, evt, n = 1 ) => new Promise( resolve => { 4 | let i = 0; 5 | 6 | emitter.on( evt, () => { 7 | i++; 8 | 9 | if( i === n ){ 10 | resolve(); 11 | } else if( i > n ){ 12 | console.error(`Expected ${evt} event ${n}x but got ${i}x`); 13 | } 14 | } ); 15 | } ); 16 | 17 | let delay = duration => new Promise( resolve => setTimeout( resolve, duration ) ); 18 | 19 | let whenAllN = ( emitters, evt, n = 1 ) => Promise.all( emitters.map( s => whenN(s, evt, n) ) ); 20 | 21 | export { 22 | whenN as when, 23 | whenAllN as whenAll, 24 | delay 25 | }; 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { env } = require('process'); 3 | const isProd = env.NODE_ENV === 'production'; 4 | const isProfile = env.PROFILE == 'true'; 5 | const isNonNil = x => x != null; 6 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 7 | const path = require('path'); 8 | 9 | const envVars = require('./src/client-env-vars.json'); 10 | 11 | // dependencies that we need to babelify ourselves 12 | const unbabelifiedDependencies = [ 13 | 'p-cancelable' 14 | ]; 15 | 16 | let conf = { 17 | entry: { 18 | bundle: './src/client/index.js', 19 | polyfills: './src/client/polyfills.js' 20 | }, 21 | 22 | output: { 23 | filename: './build/[name].js' 24 | }, 25 | 26 | devtool: 'inline-source-map', 27 | 28 | module: { 29 | rules: [ 30 | { 31 | loader: 'babel-loader', 32 | test: /\.js$/, 33 | include: [ 34 | path.resolve(__dirname, 'src'), 35 | ].concat( unbabelifiedDependencies.map( pkg => path.resolve(__dirname, 'node_modules', pkg) ) ), 36 | options: { 37 | cacheDirectory: true 38 | } 39 | } 40 | ] 41 | }, 42 | 43 | plugins: [ 44 | isProfile ? new BundleAnalyzerPlugin() : null, 45 | 46 | new webpack.DefinePlugin({ 47 | 'process.env': (() => { 48 | const obj = {}; 49 | 50 | envVars.forEach(key => { 51 | obj[key] = JSON.stringify(process.env[key]); 52 | }); 53 | 54 | return obj; 55 | })() 56 | }), 57 | 58 | new webpack.optimize.CommonsChunkPlugin({ 59 | name: 'deps', 60 | filename: './build/deps.js', 61 | minChunks( module ){ 62 | let context = module.context || ''; 63 | 64 | return context.indexOf('node_modules') >= 0 && !module.chunks.some(chunk => chunk.name === 'polyfills'); 65 | } 66 | }), 67 | 68 | new webpack.optimize.CommonsChunkPlugin({ 69 | name: 'webpackjsonp', 70 | chunks: ['deps'], 71 | minChunks: function(){ 72 | return false; 73 | } 74 | }), 75 | 76 | isProd ? new webpack.optimize.UglifyJsPlugin() : null 77 | ].filter( isNonNil ) 78 | }; 79 | 80 | module.exports = conf; 81 | --------------------------------------------------------------------------------