diff --git a/.gitignore b/.gitignore index 85dcc16d..a60329b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .git node_modules +dist +npm-debug.log +typings diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e687921..4a42b08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +### 2.4.0 +- Parse multiple comma-separated PMIDs at once into an ordered list. +- Option to add references manually for Journals, Websites, or Books. +- Search PubMed from WordPress! + - References from your search are displayed in a list similar to native PubMed and, if you find one you like, click it and it'll be inserted into your post. +- Add optional "Smart Bibliography" feature which, if enabled, allows you to... + - Insert references directly to your bibliography without having to scroll down. + - Insert references and inline citations in one step. + - Choose from a visual list of references in your bibliography if you do not choose to add citations in one step. +- If Smart Bibliography not used, the last-occurring ordered list is automatically tagged with the HTML ID `abt-smart-bib` on load to allow for more reliable tooltip rendering. +- Details for nerds: + - Full rewrite; a majority of which is using React by Facebook. + - Speed improvements & resource minification. + +### 2.3.1 +- Fixed poor rendering of tooltip close icon on mobile. +- Increase size of toucharea for tooltip close icon on mobile. + ### 2.3.0 - Tooltips on desktop and mobile given a much-needed facelift. - Tooltips now appear above or below depending on page scroll position (prevents chopping). diff --git a/academic-bloggers-toolkit.php b/academic-bloggers-toolkit.php index 5054562f..64548f3b 100644 --- a/academic-bloggers-toolkit.php +++ b/academic-bloggers-toolkit.php @@ -3,13 +3,12 @@ /* * Plugin Name: Academic Blogger's Toolkit * Plugin URI: https://wordpress.org/plugins/academic-bloggers-toolkit/ - * Description: A Wordpress plugin extending the functionality of Wordpress for Academic Blogging - * Version: 2.3.0 + * Description: A plugin extending the functionality of Wordpress for academic blogging + * Version: 2.4.0 * Author: Derek P Sifford - * Author URI: http://www.twitter.com/flightmed1 - * License: GPL3 - * License URI: https://www.gnu.org/licenses/gpl-3.0.html -*/ + * Author URI: https://github.com/dsifford + * License: GPL3 or later + */ // Assign Global Variables @@ -20,9 +19,7 @@ // Enqueue Stylesheets function abt_enqueue_styles() { - - wp_enqueue_style( 'abt_shortcodes_stylesheet', plugins_url('academic-bloggers-toolkit/inc/css/shortcodes.css') ); - + wp_enqueue_style( 'abt_frontend_styles', plugins_url('academic-bloggers-toolkit/inc/css/frontend.css') ); } add_action( 'wp_enqueue_scripts', 'abt_enqueue_styles'); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..40de2210 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + wordpress: + image: dsifford/wordpress + links: + - db + ports: + - 8080:80 + - 443:443 + volumes: + - ./dist:/app/wp-content/plugins/academic-bloggers-toolkit + environment: + DB_NAME: wordpress + DB_PASS: root + db: + image: mysql:5.7 + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: root diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..3f75b366 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,71 @@ +var gulp = require('gulp'); +var uglify = require('gulp-uglify'); +var sass = require('gulp-sass'); +var autoprefixer = require('gulp-autoprefixer'); +var cleanCSS = require('gulp-clean-css'); +var browserSync = require('browser-sync').create(); +var webpack = require('webpack-stream'); +var del = require('del'); + +gulp.task('clean', function () { + return del([ + 'dist/inc/**/*', + ]); +}); + +gulp.task('sass', function () { + return gulp.src(['./inc/**/*.scss'], { base: './' }) + .pipe(sass().on('error', sass.logError)) + .pipe(autoprefixer({ browsers: ['last 2 versions'] })) + .pipe(cleanCSS({ compatibility: 'ie10' })) + .pipe(gulp.dest('./dist')) + .pipe(browserSync.stream()); +}); + +gulp.task('webpack', function () { + return gulp.src('inc/js/frontend.ts') + .pipe(webpack(require('./webpack.config.js'))) + .pipe(gulp.dest('dist/')); +}); + +gulp.task('build', ['clean', 'webpack', 'sass'], function () { + gulp.src([ + './academic-bloggers-toolkit.php', + './CHANGELOG.md', + './LICENSE', + './readme.txt', + './inc/**/*', + '!./inc/**/*.{ts,tsx,js,css,scss,json}', + '!./**/__tests__', + '!./inc/js/utils', + ], { base: './' }) + .pipe(gulp.dest('./dist')); +}); + +gulp.task('serve', ['build'], function () { + browserSync.init({ + proxy: 'localhost:8080', + open: false, + }); + + gulp.watch(['./inc/**/*.tsx?'], ['webpack']).on('change', browserSync.reload); + gulp.watch('./inc/**/*.scss', ['sass']); + gulp.watch([ + './inc/**/*', + '!./inc/**/*.{tsx?,scss}', + '!__tests__/**/*', + ], ['build']).on('change', browserSync.reload); +}); + +gulp.task('remove-mapfiles', function (cb) { + del(['./dist/**/*.map']); + cb(); +}); + +gulp.task('minify-js', ['remove-mapfiles'], function () { + gulp.src(['./dist/**/*.js']) + .pipe(uglify()) + .pipe(gulp.dest('./dist/')); +}); + +gulp.task('deploy', ['remove-mapfiles', 'minify-js']); diff --git a/inc/css/admin.scss b/inc/css/admin.scss new file mode 100644 index 00000000..957f16db --- /dev/null +++ b/inc/css/admin.scss @@ -0,0 +1,18 @@ + +i.mce-i-abt_menu { + font: normal 25px/1 'dashicons'; + padding: 0; + vertical-align: top; + speak: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin-left: -2px; + padding-right: 2px; +} + +#abt_menubutton:hover, +#abt_menubutton.mce-active, +#abt_menubutton:hover *, +#abt_menubutton.mce-active * { + color: rgb(50, 55, 60); +} diff --git a/inc/css/shortcodes.css b/inc/css/frontend.scss similarity index 67% rename from inc/css/shortcodes.css rename to inc/css/frontend.scss index 5bdb0bf7..46f59e63 100644 --- a/inc/css/shortcodes.css +++ b/inc/css/frontend.scss @@ -1,7 +1,6 @@ /* =========== TOOLTIP ============*/ .abt_tooltip { - border: 1px solid #f1f1f1; border-radius: 3px; padding: 8px; position: absolute; @@ -10,7 +9,7 @@ background-color: #fff; box-shadow: 0 0 50px rgba(0,0,0,.35); z-index: 20; - animation: fadeInUp .2s; + animation: fadeInDown .2s; } @keyframes fadeInUp { @@ -24,6 +23,17 @@ } } +@keyframes fadeInDown { + from { + opacity: 0; + transform: translate3d(0, -10px, 0); + } + to { + opacity: 1; + transform: none; + } +} + .abt_tooltip_arrow { content: ""; position: absolute; @@ -31,8 +41,21 @@ border-width: 8px; border-style: solid; pointer-events: none; + + &.abt_arrow_up { + border-color: transparent transparent #fff; + top: -15px; + } + + &.abt_arrow_down { + border-color: #fff transparent transparent; + bottom: -15px; + } + } + + .abt_tooltip_touch_close-container { width: 50px; height: 50px; @@ -55,67 +78,60 @@ background: #555; text-indent: -9999px; transform: rotate(45deg); -} - -.abt_tooltip_touch_close:before { - width: 14px; - content: ''; - pointer-events: none; - height: 2px; - box-shadow: inset 0 0 0 32px; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); -} -.abt_tooltip_touch_close:after { - height: 14px; - content: ''; - pointer-events: none; - width: 2px; - box-shadow: inset 0 0 0 32px; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); -} + &:before,&:after { + content: ''; + left: 50%; + top: 50%; + box-shadow: inset 0 0 0 32px; + position: absolute; + transform: translate(-50%, -50%); + } + &:before { + width: 14px; + pointer-events: none; + height: 2px; + } + &:after { + height: 14px; + pointer-events: none; + width: 2px; + } +} .noselect { - -webkit-touch-callout: none; /* iOS Safari */ - -webkit-user-select: none; /* Chrome/Safari/Opera */ - -khtml-user-select: none; /* Konqueror */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE/Edge */ - user-select: none; /* non-prefixed version, currently - not supported by any browser */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } /* =====Inline Citation Style===== */ -.cite:hover { - cursor: pointer; +.abt_cite { + white-space: nowrap; } +.abt_cite:hover { + cursor: pointer; +} /* =====Peer Review Box Style===== */ -#abt_PR_boxes h3 { +.abt_PR_heading { cursor: pointer; border-bottom: 4px solid; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; border-radius: 5px; background: #f5f5f5; - outline: 0; /* Prevent div from being outlined on click */ padding: 15px; text-align: center; - margin-bottom: 10px; - margin-top: 10px; - /* Prevent text highlight on click*/ + margin: 10px 0 !important; + // Prevent text highlight on click -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -125,11 +141,11 @@ } -#abt_PR_boxes h3:hover { +.abt_PR_heading:hover { background: #f1f1f1; } -#abt_PR_boxes h3:active { +.abt_PR_heading:active { border-bottom: 1px solid; padding-top: 18px; } @@ -139,7 +155,7 @@ float:left; width: 30%; position: relative; - top: 0px; + top: 0; text-align: center; font-size: 10px; margin-bottom: 10px; @@ -154,24 +170,21 @@ display: block; margin-left:auto; margin-right:auto; + height: auto; + min-width: 100px; } .abt_chat_bubble { display: inline-block; position: relative; width: 63%; - height: auto; min-height: 100px; - margin-right: 0px; - margin-left: 0px; + margin: auto 0 20px 0; background: #F6F6F6; - -webkit-border-radius: 8px; - -moz-border-radius: 8px; border-radius: 8px; padding: 15px; border-left: 1px solid #c7c7c7; border-top: 1px solid #c7c7c7; - margin-bottom: 20px; box-shadow: 1px 1px 1px #C7C7C7; } @@ -181,8 +194,6 @@ background-color: #F6F6F6; width: 20px; height: 20px; - -ms-transform: rotate(45deg); - -webkit-transform: rotate(45deg); transform: rotate(45deg); z-index: 1; margin-top: -15px; @@ -192,6 +203,11 @@ border-bottom: 1px solid #c7c7c7; } +// HORIZONTAL RULE VISIBLE IN EDITOR ONLY +.abt_editor-only { + display: none; +} + @media only screen and (max-width: 620px) { .abt_PR_info { display:block; @@ -211,8 +227,6 @@ border-bottom: 1px solid #c7c7c7; position: absolute; top: -30px; - -ms-transform: rotate(45deg); - -webkit-transform: rotate(45deg); transform: rotate(45deg); z-index: 1; diff --git a/inc/images/book.png b/inc/images/book.png deleted file mode 100644 index 73a7d839..00000000 Binary files a/inc/images/book.png and /dev/null differ diff --git a/inc/images/silhouette.png b/inc/images/silhouette.png deleted file mode 100644 index da78ec5e..00000000 Binary files a/inc/images/silhouette.png and /dev/null differ diff --git a/inc/js/ABT.d.ts b/inc/js/ABT.d.ts new file mode 100644 index 00000000..b05ceb0a --- /dev/null +++ b/inc/js/ABT.d.ts @@ -0,0 +1,162 @@ +interface Window { + tinyMCE: tinyMCE +} + +interface tinyMCE { + DOM: any + EditorManager: any + EditorObservable: any + Env: any + WindowManager: any + activeEditor: tinyMCEEditor + add: (a:any) => any + dom: any + editors: any[] + remove: (e?: any) => void +} + +interface tinyMCEEditor { + addButton: (any) => any + buttons: any + container: any + contentDocument: HTMLDocument + contentWindow: Window + controlManager: any + dom: any + editorCommands: any + editorContainer: any + editorManager: any + editorUpload: any + plugins: any + settings: any + target: any + windowManager: TinyMCEWindowManager + wp: any +} + +interface TinyMCEWindowManager { + alert: (a?:any) => any + close: (a?:any) => any + confirm: (a?:any) => any + createInstance: (a?:any) => any + editor: tinyMCEEditor + getParams: (a?:any) => any + getWindows: (a?:any) => any + onClose: any + onOpen: any + open: (a?:any) => any + parent: any + setParams: (a?:any) => any + windows: any + wp: any +} + +interface TinyMCEMenuItem { + text: string + menu?: TinyMCEMenuItem[] + onclick?: (e?: Event) => void + disabled?: boolean + id?: string +} + +interface TinyMCEWindowElement { + type: string + name: string + label: string + value: string + tooltip?: string +} + +interface TinyMCEWindowMangerObject { + title: string + width: number + height: any + body?: TinyMCEWindowElement[] + url?: string + onclose?: (e?) => void +} + +interface TinyMCEPluginButton { + type: string + image: string + title: string + icon: boolean + menu: TinyMCEMenuItem[] + onclick?: (e?: Event) => void +} + +interface Author { + authtype?: string + clusterid?: string + name?: string + firstname?: string + lastname?: string + middleinitial?: string +} + +interface CommonMeta { + title: string + source: string + pubdate: string +} + +interface BookMeta extends CommonMeta { + chapter: string + edition: string + location: string + pages: string +} + +interface JournalMeta extends CommonMeta { + volume: string + issue: string + pages: string +} + +interface WebsiteMeta extends CommonMeta { + url: string + updated: string + accessed: string +} + +interface ManualDataObj { + authors: Author[] + meta: { + book: BookMeta + journal: JournalMeta + website: WebsiteMeta + } + type: 'journal'|'website'|'book' +} + +interface ReferenceFormData { + addManually: boolean + attachInline: boolean + citationFormat: string + includeLink: boolean + manualData: ManualDataObj + pmidList: string +} + +interface ReferenceObj { + authors: Author[] + lastauthor: string + pages: string + pubdate: string + source: string + title: string + accessdate?: string + chapter?: string + edition?: string + fulljournalname?: string + issue?: string + location?: string + updated?: string + url?: string + volume?: string +} + +interface ReferencePayload { + [i: number]: ReferenceObj + uids?: string[] +} diff --git a/inc/js/components/CitationWindow.tsx b/inc/js/components/CitationWindow.tsx new file mode 100644 index 00000000..5940ca70 --- /dev/null +++ b/inc/js/components/CitationWindow.tsx @@ -0,0 +1,173 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import Modal from '../utils/Modal' +import { + parseInlineCitationString, + parseCitationNumArray +} from '../utils/HelperFunctions'; + +interface State { + citeText: string + citeArray: number[] +} + +export default class CitationWindow extends React.Component<{}, State> { + + private modal: Modal = new Modal('Inline Citation'); + private editorDOM: HTMLDocument = top.tinyMCE.activeEditor.dom.doc; + private wm = top.tinyMCE.activeEditor.windowManager; + private refList: HTMLOListElement|boolean; + + constructor() { + super(); + let smartBib = this.editorDOM.getElementById('abt-smart-bib'); + this.refList = (smartBib as HTMLOListElement) || false; + this.state = { + citeText: '', + citeArray: [], + } + } + + componentDidMount() { + this.modal.resize(); + } + + parser(input: number[]|string): string|number[] { + if (Array.isArray(input)) { + return parseInlineCitationString(input); + } + return parseCitationNumArray(input as string); + } + + handleSubmit(e: Event) { + e.preventDefault(); + this.wm.setParams({ data: this.state.citeArray.sort((a, b) => a - b) }); + this.wm.close(); + } + + handleChange(e: Event) { + let val = (e.currentTarget as HTMLInputElement).value; + let newStateArr = Array.from(new Set(this.parser(val) as number[])); + this.setState(Object.assign({}, this.state, { citeText: val, citeArray: newStateArr })); + } + + handleClick(e: Event) { + let selected = e.currentTarget as HTMLDivElement; + let selectedNum: number = parseInt(selected.dataset['citenum']); + let indexOfNum: number = this.state.citeArray.indexOf(selectedNum); + let newStateArray: number[]; + let newStateString: string; + + // Take care of the number array first + switch (indexOfNum) { + case -1: + newStateArray = + this.state.citeArray + .concat(selectedNum) + .sort((a, b) => a - b ); + break; + default: + newStateArray = [ + ...this.state.citeArray.slice(0, indexOfNum), + ...this.state.citeArray.slice(indexOfNum + 1) + ]; + } + + // Parse new input string + newStateString = this.parser(newStateArray) as string; + + this.setState({ + citeText: newStateString, + citeArray: newStateArray, + }); + } + + render() { + return( +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ { this.refList && + + } +
+ ) + } + +} + +interface RefListProps { + list: HTMLCollection + modal: Modal + clickHandler: Function + citeArray: number[] +} + +class ReferenceList extends React.Component { + + constructor(props) { + super(props); + } + + componentDidMount() { + this.props.modal.resize(); + } + + render() { + + return( +
+ {Object.keys(this.props.list).map((key, i) => { + + let thisClass: string = 'cite-row'; + thisClass += i % 2 === 0 ? ' even' : ''; + thisClass += this.props.citeArray.indexOf(i+1) !== -1 ? ' cite-selected' : ''; + + return( +
${i+1}. ` + + (this.props.list[key] as HTMLLIElement).innerHTML} + } + onClick={this.props.clickHandler} + data-citenum={i+1} /> + ) + })} +
+ ) + } + +} + + + +ReactDOM.render( + , + document.getElementById('main-container') +); diff --git a/inc/js/components/PubmedWindow.tsx b/inc/js/components/PubmedWindow.tsx new file mode 100644 index 00000000..75d86074 --- /dev/null +++ b/inc/js/components/PubmedWindow.tsx @@ -0,0 +1,209 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import Modal from '../utils/Modal'; +import { PubmedQuery } from '../utils/PubmedAPI'; + +declare var wm; + + +class PubmedWindow extends React.Component +<{}, {query: string, results: Object[], page: number}> { + + private modal: Modal = new Modal('Search PubMed for Reference'); + private wm: any = top.window.tinyMCE.activeEditor.windowManager.windows[top.window.tinyMCE.activeEditor.windowManager.windows.length - 1]; + + constructor() { + super(); + this.state = { + query: '', + results: [], + page: 0, + }; + } + + componentDidMount() { + this.modal.resize(); + } + + _handleSubmit(e: Event) { + e.preventDefault(); + PubmedQuery(this.state.query, (data: Object[]|Error) => { + if ((data as Error).name == 'Error') { + top.tinyMCE.activeEditor.windowManager.alert((data as Error).message); + return; + } + this.setState({ + query: '', + results: (data as Object[]), + page: 1, + }) + this.modal.resize(); + }); + } + + _changeHandler(e: Event) { + this.setState({ + query: (e.target as HTMLInputElement).value, + results: this.state.results, + page: this.state.page, + }); + } + + _handlePagination(e: Event) { + e.preventDefault(); + + let page: number = this.state.page; + page = (e.target as HTMLInputElement).id === 'next' + ? page + 1 + : page - 1; + + this.setState({ + query: this.state.query, + results: this.state.results, + page + }); + setTimeout(() => { + this.modal.resize(); + }, 200); + } + + _insertPMID(e: Event) { + this.wm.data['pmid'] = (e.target as HTMLInputElement).dataset['pmid']; + this.wm.submit(); + } + + render() { + return ( +
+
+
+ + +
+
+ { this.state.results.length !== 0 && + { + if ( i < (this.state.page * 5) && ((this.state.page * 5) - 6) < i ) { + return true; + } + })} /> + } + { this.state.results.length !== 0 && + + } +
+ ) + } +} + +const ResultList = ({ + results, + onClick +}) => { + return( +
+ {results.map((result, i: number) => +
+
+ +
+ {result.authors.filter((el, i) => i < 3).map(el => el.name).join(', ')} +
+
+ {result.source} | {result.pubdate} +
+
+
+ +
+
+ )} +
+ ) +} + +const Paginate = ({ + page, + onClick, + resultLength, +}) => { + return ( +
+
+ +
+
+ 3 || page === 0 || ((page + 1) * 5) > resultLength } + onClick={onClick} + value='Next' /> +
+
+ ) +} + + +function generatePlaceholder(): string { + + let options = [ + "Ioannidis JP[Author - First] AND meta research", + 'Brohi K[Author - First] AND "acute traumatic coagulopathy"', + "Dunning[Author] AND Kruger[Author] AND incompetence", + "parachute use AND death prevention AND BMJ[Journal]", + "obediance AND Milgram S[Author - First]", + "tranexamic acid AND trauma NOT arthroscopy AND Lancet[Journal]", + 'Watson JD[Author] AND Crick FH[Author] AND "nucleic acid"', + 'innovation OR ("machine learning" OR "deep learning") AND healthcare', + "injuries NOT orthopedic AND hemorrhage[MeSH]", + "resident OR student AND retention", + ]; + + return options[Math.ceil(Math.random() * 10) - 1]; + +} + + +ReactDOM.render( + , + document.getElementById('main-container') +); diff --git a/inc/js/components/ReferenceWindow.tsx b/inc/js/components/ReferenceWindow.tsx new file mode 100644 index 00000000..39f978b1 --- /dev/null +++ b/inc/js/components/ReferenceWindow.tsx @@ -0,0 +1,670 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import Modal from '../utils/Modal'; + +interface Author { + firstname: string + lastname: string + middleinitial: string +} + +interface CommonMeta { + title: string + source: string + pubdate: string +} + +interface JournalMeta extends CommonMeta { + volume: string + issue: string + pages: string +} + +interface WebsiteMeta extends CommonMeta { + url: string + updated: string + accessed: string +} + +interface BookMeta extends CommonMeta { + chapter: string + edition: string + location: string + pages: string +} + +interface ManualMeta { + journal: JournalMeta, + website: WebsiteMeta, + book: BookMeta, +} + +interface ManualPayload { + type: 'journal'|'website'|'book' + authors: Author[] + meta: ManualMeta +} + +interface State { + pmidList: string + citationFormat: string + includeLink: boolean + attachInline: boolean + addManually: boolean + manualData: ManualPayload +} + + +class ReferenceWindow extends React.Component<{}, State> { + + private modal: Modal = new Modal('Insert Formatted Reference'); + private smartBibIsEnabled = + (top.tinyMCE.activeEditor.dom.doc as Document).getElementById('abt-smart-bib'); + + constructor() { + super(); + this.state = { + pmidList: '', + citationFormat: top.tinyMCE.activeEditor.windowManager.windows[0].settings.params.preferredStyle || 'ama', + includeLink: false, + attachInline: false, + addManually: false, + manualData: { + type: 'journal', + authors: [ + { firstname: '', lastname: '', middleinitial: '', }, + ], + meta: { + journal: { + title: '', + source: '', + pubdate: '', + volume: '', + issue: '', + pages: '', + }, + website: { + title: '', + source: '', + pubdate: '', + url: '', + updated: '', + accessed: '', + }, + book: { + title: '', + source: '', + pubdate: '', + chapter: '', + edition: '', + location: '', + pages: '', + }, + } + }, + } + } + + componentDidMount() { + this.modal.resize(); + } + + componentDidUpdate() { + this.modal.resize(); + } + + handleButtonClick(e: MouseEvent) { + let id = (e.target as HTMLInputElement).id; + + switch(id) { + case 'searchPubmed': + let wm = top.tinyMCE.activeEditor.windowManager; + wm.open({ + title: 'Search PubMed for Reference', + url: wm.windows[0].settings.params.baseUrl + 'pubmed-window.html', + width: 600, + height: 100, + onsubmit: (e: any) => { + let newList: string = e.target.data.pmid; + + // If the current PMID List is not empty, add PMID to it + if (this.state.pmidList !== '') { + let combinedInput = this.state.pmidList.split(','); + combinedInput.push(e.target.data.pmid); + newList = combinedInput.join(','); + } + + this.setState(Object.assign({}, this.state, { pmidList: newList })); + }} + ); + break; + case 'addManually': + this.setState(Object.assign({}, this.state, { addManually: !this.state.addManually })); + break; + } + + } + + handleSubmit(e: Event) { + e.preventDefault(); + let wm = top.tinyMCE.activeEditor.windowManager; + wm.setParams({ data: this.state }); + wm.close(); + } + + consumeChange(e: Event) { + + // Switch on the type of input element and create a new, non-mutated + // state object to apply the result of the state change. + let id: string = (e.target as HTMLElement).id; + let tagName: string = (e.target as HTMLElement).tagName; + let newState = {}; + + switch (tagName) { + case 'INPUT': + let type: string = (e.target as HTMLInputElement).type; + + switch(type) { + case 'text': + newState[id] = (e.target as HTMLInputElement).value; + break; + case 'checkbox': + newState[id] = (e.target as HTMLInputElement).checked; + break; + } + break; + case 'SELECT': + newState[id] = (e.target as HTMLSelectElement).value; + break; + } + + this.setState(Object.assign({}, this.state, newState)); + } + + consumeManualDataChange(e: Event) { + + let type: string = e.type; + let newData = Object.assign({}, this.state.manualData); + + switch(type) { + case 'AUTHOR_DATA_CHANGE': + newData.authors = (e as CustomEvent).detail; + break; + case 'ADD_AUTHOR': + newData.authors = [...newData.authors, { firstname: '', lastname: '', middleinitial: '', }]; + break + case 'REMOVE_AUTHOR': + let removeNum: number = parseInt((e as CustomEvent).detail); + newData.authors = [ + ...newData.authors.slice(0, removeNum), + ...newData.authors.slice(removeNum + 1), + ]; + break; + case 'TYPE_CHANGE': + newData.type = (e as CustomEvent).detail; + break; + case 'META_CHANGE': + newData.meta = (e as CustomEvent).detail; + break; + } + + this.setState(Object.assign({}, this.state, { manualData: newData })); + + } + + render() { + return( +
+
+ { !this.state.addManually && + + } + { this.state.addManually && + + } + + + +
+ ); + } + +} + + +class PMIDInput extends React.Component<{pmidList: string, onChange: Function},{}> { + + refs: { + [key: string]: Element + pmidInput: HTMLInputElement + } + + componentDidMount() { + (ReactDOM.findDOMNode(this.refs.pmidInput) as HTMLInputElement).focus() + } + + render() { + let sharedStyle = { + padding: '5px', + } + return( +
+
+ +
+ +
+ +
+
+ +
+
+ ) + } + +} + + +const RefOptions = ({ + attachInline, + citationFormat, + onChange, + smartBibIsEnabled, +}) => { + let commonStyle = { padding: '5px' } + return( +
+
+
+ +
+
+ +
+
+ { smartBibIsEnabled && +
+
+ +
+
+ +
+
+ } +
+ ); +} + + +const ActionButtons = ({ + addManually, + onClick +}) => { + + const rowStyle = { + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-around', + }; + + const spanStyle = { + borderRight: 'solid 2px #ccc', + height: 25, + margin: '0 15px 0 10px', + }; + + const buttonStyle = { margin: '0 5px' }; + + const submitStyle = { + flexGrow: 1, + margin: '0 15px 0 0' + }; + + return( +
+ + + + + +
+ ) +} + + + + +class ManualEntryContainer extends React.Component<{ + manualData: ManualPayload, + onChange +},{}> { + + constructor(props) { + super(props); + } + + + typeChange(e) { + let event = new CustomEvent('TYPE_CHANGE', { detail: e.target.value }); + this.props.onChange(event); + } + + authorChange(e) { + let type = e.target.dataset['nametype']; + let authNumber: number = parseInt(e.target.dataset['authornum']); + let newAuthorList = [...this.props.manualData.authors]; + newAuthorList[authNumber][type] = e.target.value; + let event = new CustomEvent('AUTHOR_DATA_CHANGE', {detail: newAuthorList}) + this.props.onChange(event); + + } + + addAuthor(e) { + let event = new CustomEvent('ADD_AUTHOR'); + this.props.onChange(event); + } + + removeAuthor(e) { + let authornum = e.target.dataset['authornum']; + let event = new CustomEvent('REMOVE_AUTHOR', { detail: authornum }); + this.props.onChange(event); + } + + handleMetaChange(e) { + let newMeta = Object.assign({}, this.props.manualData.meta); + newMeta[this.props.manualData.type][e.target.dataset['metakey']] = e.target.value; + let event = new CustomEvent('META_CHANGE', { detail: newMeta }); + this.props.onChange(event); + } + + render() { + return( +
+ + + +
+ ) + } + +} + +const ManualSelection = ({ + value, + onChange, +}) => { + const commonStyle = { padding: '5px' }; + return( +
+
+ +
+
+ +
+
+ ) +} + +const Authors = ({ + authorList, + removeAuthor, + onChange, + addAuthor, + type, +}) => { + const inputStyle = { + flex: 1, + padding: '0 5px', + }; + const commonStyle = { + padding: '0 5px' + } + return( +
+
+ Author Name(s) +
+ {authorList.map((author: Author, i: number) => +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ )} +
+ +
+
+ ) +} + + +const MetaFields = ({type, meta, onChange,} : +{ + type: string + meta: ManualMeta + onChange: Function +}) => { + + const outerFlex = { + display: 'flex', + flexDirection: 'column', + } + + const innerFlex = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + } + + let displayMeta: Object[] = []; + let title: string = type[0].toUpperCase() + type.substring(1, type.length); + + switch(type) { + case 'journal': + displayMeta = [ + { meta: meta.journal.title, key: 'title', label: 'Title', required: true }, + { meta: meta.journal.source, key: 'source', label: 'Journal Name', required: true }, + { meta: meta.journal.pubdate, key: 'pubdate', label: 'Year Published', pattern: '[1-2][0-9]{3}', required: true }, + { meta: meta.journal.volume, key: 'volume', label: 'Volume', pattern: '[0-9]{1,5}', required: false }, + { meta: meta.journal.issue, key: 'issue', label: 'Issue', pattern: '^[0-9]{1,4}', required: false }, + { meta: meta.journal.pages, key: 'pages', label: 'Pages', pattern: '^([0-9]{1,4}(?:-[0-9]{1,4}$)?)', required: true } + ]; + break; + case 'website': + displayMeta = [ + { meta: meta.website.title, key: 'title', label: 'Content Title', required: true }, + { meta: meta.website.source, key: 'source', label: 'Website Title', required: true }, + { meta: meta.website.pubdate, key: 'pubdate', label: 'Published Date', placeholder: 'MM/DD/YYYY', pattern: '[0-1][0-9][-/][0-3][0-9][-/][1-2][0-9]{3}', required: true }, + { meta: meta.website.url, key: 'url', label: 'URL', required: true }, + { meta: meta.website.updated, key: 'updated', label: 'Updated Date', placeholder: 'MM/DD/YYYY', pattern: '[0-1][0-9][-/][0-3][0-9][-/][1-2][0-9]{3}', required: false }, + { meta: meta.website.accessed, key: 'accessed', label: 'Accessed Date', placeholder: 'MM/DD/YYYY', pattern: '[0-1][0-9][-/][0-3][0-9][-/][1-2][0-9]{3}', required: false }, + ]; + break; + case 'book': + displayMeta = [ + { meta: meta.book.title, key: 'title', label: 'Book Title', required: true }, + { meta: meta.book.source, key: 'source', label: 'Publisher', required: true }, + { meta: meta.book.pubdate, key: 'pubdate', label: 'Copyright Year', pattern: '[1-2][0-9]{3}', required: true }, + { meta: meta.book.chapter, key: 'chapter', label: 'Chapter/Section', required: false }, + { meta: meta.book.edition, key: 'edition', label: 'Edition', required: false }, + { meta: meta.book.location, key: 'location', label: 'Publisher Location', required: false }, + { meta: meta.book.pages, key: 'pages', label: 'Pages', pattern: '^([0-9]{1,4}(?:-[0-9]{1,4}$)?)', required: false }, + ]; + break; + } + + return( +
+
+ {title} Information +
+
+ {displayMeta.map((item, i: number) => +
+
+ +
+
+ +
+
+ )} +
+
+ ) +} + + + +ReactDOM.render( + , + document.getElementById('main-container') +) diff --git a/inc/js/peer-review.ts b/inc/js/frontend.ts similarity index 67% rename from inc/js/peer-review.ts rename to inc/js/frontend.ts index b4eb6db0..8a8c71ca 100644 --- a/inc/js/peer-review.ts +++ b/inc/js/frontend.ts @@ -2,28 +2,29 @@ declare var DocumentTouch; -module ABT_Frontend { +namespace ABT_Frontend { export class Accordion { - private _headings: NodeList; + private _headings: NodeListOf; constructor() { - this._headings = document.querySelectorAll('#abt_PR_boxes > h3'); + this._headings = (document.getElementsByClassName('abt_PR_heading') as NodeListOf); - for (let heading of (this._headings)) { - let reviewContent: HTMLElement = heading.nextSibling; - reviewContent.style.display = 'none'; + for (let i = 0; i < this._headings.length; i++) { + let currentHeading = this._headings[i]; + let reviewContent = (currentHeading.nextElementSibling as HTMLDivElement); - heading.addEventListener('click', this._clickHandler); + reviewContent.style.display = 'none'; + currentHeading.addEventListener('click', this._clickHandler); } } private _clickHandler(e: Event): void { - let targetContent = e.srcElement.nextSibling; + let targetContent = (e.srcElement.nextSibling as HTMLDivElement); // If targetContent already visible, hide it and exit if (targetContent.style.display != 'none') { @@ -31,16 +32,20 @@ module ABT_Frontend { return; } - let accordionNodes = e.srcElement.parentElement.childNodes; - let element: HTMLElement; + let accordionChildren = e.srcElement.parentElement.children; + + for (let i = 0; i < accordionChildren.length; i++) { + + let currentElement = accordionChildren[i] as HTMLElement; - for (element of accordionNodes) { - if (element.tagName != 'DIV') { continue; } - if (element.previousSibling === e.srcElement) { - element.style.display = ''; + if (currentElement.tagName != 'DIV') { continue; } + + if (currentElement.previousSibling === e.srcElement) { + currentElement.style.display = ''; continue; } - element.style.display = 'none'; + + currentElement.style.display = 'none'; } } @@ -53,13 +58,12 @@ module ABT_Frontend { public static timer: number; constructor() { - let referenceList: HTMLOListElement = this._getReferenceList(); - let citationList: NodeListOf = document.querySelectorAll('span.cite'); - let citation: HTMLSpanElement; + let referenceList = (document.getElementById('abt-smart-bib') as HTMLOListElement); + let citationList = document.getElementsByClassName('abt_cite') - for (citation of citationList) { + for (let i = 0; i < citationList.length; i++) { - let citeNums: number[] = JSON.parse(citation.dataset['reflist']); + let citeNums: number[] = JSON.parse((citationList[i] as HTMLSpanElement).dataset['reflist']); let citationHTML = citeNums.map((citeNum: number): string => { // Correct for zero-based index citeNum--; @@ -78,14 +82,14 @@ module ABT_Frontend { }); // Save CSV string of citenums as data attr 'citations' - citation.dataset['citations'] = citationHTML.join(''); + (citationList[i] as HTMLSpanElement).dataset['citations'] = citationHTML.join(''); // Conditionally create tooltip based on device if (this._isTouchDevice()) { - citation.addEventListener('touchstart', this._createTooltip.bind(this)); + citationList[i].addEventListener('touchstart', this._createTooltip.bind(this)); } else { - citation.addEventListener('mouseover', this._createTooltip.bind(this)); - citation.addEventListener('mouseout', this._destroyTooltip); + citationList[i].addEventListener('mouseover', this._createTooltip.bind(this)); + citationList[i].addEventListener('mouseout', this._destroyTooltip); } } @@ -93,23 +97,13 @@ module ABT_Frontend { } - private _getReferenceList(): HTMLOListElement { - let orderedLists: NodeListOf = document.getElementsByTagName('ol'); - for (let i = (orderedLists.length - 1); i >= 0; i--){ - if (orderedLists[i].parentElement.className !== 'abt_chat_bubble') { - return orderedLists[i]; - } - } - } - - private _isTouchDevice(): boolean { return true == ("ontouchstart" in window || (window).DocumentTouch && document instanceof DocumentTouch); } private _createTooltip(e: Event): void { - + e.preventDefault(); clearTimeout(Citations.timer); let preExistingTooltip: HTMLElement = document.getElementById('abt_tooltip'); @@ -132,6 +126,7 @@ module ABT_Frontend { let closeButton: HTMLDivElement = document.createElement('div'); let touchContainer: HTMLDivElement = document.createElement('div'); + touchContainer.className = 'abt_tooltip_touch_close-container'; closeButton.className = 'abt_tooltip_touch_close'; touchContainer.addEventListener('touchend', () => tooltip.remove()); @@ -139,9 +134,9 @@ module ABT_Frontend { tooltip.style.left = '0'; tooltip.style.right = '0'; tooltip.style.maxWidth = '90%' + touchContainer.appendChild(closeButton); tooltip.appendChild(touchContainer); - // tooltip.appendChild(closeButton); document.body.appendChild(tooltip); tooltip.appendChild(tooltipArrow); @@ -162,13 +157,14 @@ module ABT_Frontend { // Set tooltip above or below based on window position + set arrow position if ((rect.top - tooltip.offsetHeight) < 0) { + // On bottom - Upwards arrow tooltip.style.top = (rect.bottom + window.scrollY + 5) + 'px'; - tooltipArrow.style.top = '-15px'; - tooltipArrow.style.borderColor = 'transparent transparent #fff'; + tooltip.style.animation = 'fadeInUp .2s'; + tooltipArrow.classList.add('abt_arrow_up'); } else { + // On top - Downwards arrow tooltip.style.top = (rect.top + window.scrollY - tooltip.offsetHeight - 5) + 'px'; - tooltipArrow.style.bottom = '-15px'; - tooltipArrow.style.borderColor = '#fff transparent transparent'; + tooltipArrow.classList.add('abt_arrow_down'); } tooltip.style.visibility = ''; @@ -196,7 +192,7 @@ if (document.readyState != 'loading'){ function frontendJS() { - new ABT_Frontend.Citations(); - new ABT_Frontend.Accordion(); + new ABT_Frontend.Citations; + new ABT_Frontend.Accordion; } diff --git a/inc/js/meta-box-image.js b/inc/js/meta-box-image.js deleted file mode 100644 index b0d00d80..00000000 --- a/inc/js/meta-box-image.js +++ /dev/null @@ -1,146 +0,0 @@ - -if (document.readyState != 'loading'){ - editorJS(); -} else { - document.addEventListener('DOMContentLoaded', editorJS); -} - -function editorJS() { - - var authorNameInputs = document.querySelectorAll('input[id^=author_name_]'); - var authorResponseTables = document.querySelectorAll('table[id^=author_response]'); - var reviewContent = document.querySelectorAll('textarea[id^=peer_review_content_]'); - var responseContent = document.querySelectorAll('textarea[id^=author_content_]'); - - // Inital actions on load - for (var i = 0; i < 3; i++) { - - // Hide empty author responses - if (authorNameInputs[i].value == '') { - authorResponseTables[i].style.display = 'none'; - } - - // Replace
and

tags with actual line breaks on post edit screen - reviewContent[i].value = reviewContent[i].value.replace(/(
)|(
)|(

)|(<\/p>)/, "").replace(/(
)|(
)|(

)|(<\/p>)/g, "\r"); - responseContent[i].value = responseContent[i].value.replace(/(
)|(
)|(

)|(<\/p>)/, "").replace(/(
)|(
)|(

)|(<\/p>)/g, "\r"); - - } - - - // ================================================== - // SELECT BOX HANDLER - // ================================================== - - // Show/hide nodes based on select option - var selectBox = document.querySelector('#reviewer_selector'); - var selectDivs = document.querySelectorAll('#peer_review_metabox_wrapper > div'); - selectBox.addEventListener('change', selectHandler); - - // Simulate a change event on initial page load - var simulatedChange = new Event('change'); - selectBox.dispatchEvent(simulatedChange); - - function selectHandler(e) { - for (var i = 0; i < selectDivs.length; i++) { - selectDivs[i].style.display = 'none'; - } - - switch (e.target.value) { - case '3': - selectDivs['2'].style.display = ''; - case '2': - selectDivs['1'].style.display = ''; - case '1': - selectDivs['0'].style.display = ''; - } - - }; - - - // ================================================== - // AUTHOR RESPONSE BUTTON HANDLER - // ================================================== - - // Show hide author response based on button toggle - var toggleButtons = document.querySelectorAll('input[id^=author_response_button]'); - for (var i = 0; i < toggleButtons.length; i++) { - - toggleButtons[i].addEventListener('click', function(e) { - - currentIndex = parseInt(e.target.id.slice(-1)) - 1; - - if (authorResponseTables[currentIndex].style.display == 'none') { - authorResponseTables[currentIndex].style.display = 'block'; - return; - } - - authorResponseTables[currentIndex].style.display = 'none'; - - }); - } - - //===================================================== - // MEDIA UPLOAD HANDLER - //===================================================== - - // Instantiates the variable that holds the media library frame. - var abt_meta_image_frames = [null, null, null, null, null, null]; - wp.media.frames.abt_meta_image_frames = { - '1': null, - '2': null, - '3': null, - '4': null, - '5': null, - '6': null - }; - - var headshotButtons = document.querySelectorAll('input[id^=headshot_image_button]'); - for (var i = 0; i < headshotButtons.length; i++) { - - headshotButtons[i].addEventListener('click', function(e) { - - var headshotImageInput; - var i = parseInt(e.target.id.slice(-1)) - 1; - switch (i) { - case 0: - case 1: - case 2: - headshotImageInput = document.querySelector('#reviewer_headshot_image_' + (i+1).toString()); - break; - case 3: - case 4: - case 5: - headshotImageInput = document.querySelector('#author_headshot_image_' + (i-2).toString()); - break; - } - - if (abt_meta_image_frames[i]) { - abt_meta_image_frames[i].open(); - return; - } - - abt_meta_image_frames[i] = wp.media.frames.abt_meta_image_frames[i] = wp.media({ - title: meta_image.title, - button: { text: meta_image.button }, - library: { type: 'image' } - }); - - // Runs when an image is selected. - abt_meta_image_frames[i].on('select', function(){ - - // Grabs the attachment selection and creates a JSON representation of the model. - var media_attachment = abt_meta_image_frames[i].state().get('selection').first().toJSON(); - - // Sends the attachment URL to our custom image input field. - headshotImageInput.value = media_attachment.url; - - }); - - // Opens the media library frame. - abt_meta_image_frames[i].open(); - - }); - - } - -} diff --git a/inc/js/metaboxes.ts b/inc/js/metaboxes.ts new file mode 100644 index 00000000..f1d15edc --- /dev/null +++ b/inc/js/metaboxes.ts @@ -0,0 +1,142 @@ +declare var wp, ABT_locationInfo; + +class PeerReviewMetabox { + + public inputs = { + authorNames: [] as HTMLInputElement[], + toggleAuthorResponse: [] as HTMLInputElement[], + imageButtons: [] as HTMLInputElement[], + } + + public containers = { + reviews: [] as HTMLDivElement[], + } + + public tables = { + authorResponses: [] as HTMLTableElement[], + } + + public textareas = { + reviewContent: [] as HTMLTextAreaElement[], + responseContent: [] as HTMLTextAreaElement[], + } + + constructor() { + this._initFields(); + wp.media.frames.abt_reviewer_photos = [null, null, null, null, null, null]; + + this._selectHandler(); + document.getElementById('reviewer_selector').addEventListener('change', this._selectHandler.bind(this)); + } + + + private _initFields(): void { + + for (let i = 0; i < 3; i++) { + this.inputs.authorNames[i] = document.getElementById(`author_name_${i + 1}`) as HTMLInputElement; + this.inputs.toggleAuthorResponse[i] = document.getElementById(`author_response_button_${i + 1}`) as HTMLInputElement; + this.inputs.imageButtons[i] = document.getElementById(`headshot_image_button_${i + 1}`) as HTMLInputElement; + this.inputs.imageButtons[i+3] = document.getElementById(`headshot_image_button_${i+3 + 1}`) as HTMLInputElement; + this.tables.authorResponses[i] = document.getElementById(`author_response_${i + 1}`) as HTMLTableElement; + this.textareas.reviewContent[i] = document.getElementById(`peer_review_content_${i + 1}`) as HTMLTextAreaElement; + this.textareas.responseContent[i] = document.getElementById(`author_content_${i + 1}`) as HTMLTextAreaElement; + this.containers.reviews[i] = document.getElementById(`tabs-${i + 1}`) as HTMLDivElement + + // Hide empty author response tables + if (this.inputs.authorNames[i].value === '') { + this.tables.authorResponses[i].style.display = 'none'; + } + + // Reformat textareas to remove raw HTML + this.textareas.reviewContent[i].value = this.textareas.reviewContent[i].value + .replace(/(
)|(
)|(

)|(<\/p>)/, "") + .replace(/(
)|(
)|(

)|(<\/p>)/g, "\r"); + + this.textareas.responseContent[i].value = this.textareas.responseContent[i].value + .replace(/(
)|(
)|(

)|(<\/p>)/, "") + .replace(/(
)|(
)|(

)|(<\/p>)/g, "\r"); + + this.inputs.toggleAuthorResponse[i].addEventListener('click', this._toggleAuthorResponse.bind(this)); + this.inputs.imageButtons[i].addEventListener('click', this._mediaUploadHandler.bind(this)) + this.inputs.imageButtons[i+3].addEventListener('click', this._mediaUploadHandler.bind(this)) + + } + } + + private _selectHandler(e?: Event): void { + + for (let el of this.containers.reviews ) { + el.style.display = 'none'; + } + + let selectBox = document.getElementById('reviewer_selector') as HTMLSelectElement + switch (selectBox.value) { + case '3': + this.containers.reviews[2].style.display = ''; + case '2': + this.containers.reviews[1].style.display = ''; + case '1': + this.containers.reviews[0].style.display = ''; + } + } + + private _toggleAuthorResponse(e: Event): void { + let i = parseInt(e.srcElement.id.slice(-1)) - 1; + let response = this.tables.authorResponses[i]; + response.style.display = response.style.display === 'none' ? '' : 'none'; + } + + private _mediaUploadHandler(e: Event): void { + + let headshotImageInput: HTMLInputElement; + let i: number = parseInt(e.srcElement.id.slice(-1)) - 1; + switch (i) { + case 0: + case 1: + case 2: + headshotImageInput = document.getElementById(`reviewer_headshot_image_${i+1}`) as HTMLInputElement; + break; + case 3: + case 4: + case 5: + headshotImageInput = document.getElementById(`author_headshot_image_${i-2}`) as HTMLInputElement; + break; + } + + if (wp.media.frames.abt_reviewer_photos[i]) { + wp.media.frames.abt_reviewer_photos[i].open(); + return; + } + + wp.media.frames.abt_reviewer_photos[i] = wp.media({ + title: 'Choose or Upload an Image', + button: { text: 'Use this image' }, + library: { type: 'image' } + }); + + // Runs when an image is selected. + wp.media.frames.abt_reviewer_photos[i].on('select', function(){ + + // Grabs the attachment selection and creates a JSON representation of the model. + let media_attachment = wp.media.frames.abt_reviewer_photos[i].state().get('selection').first().toJSON(); + + // Sends the attachment URL to our custom image input field. + headshotImageInput.value = media_attachment.url; + + }); + + // Opens the media library frame. + wp.media.frames.abt_reviewer_photos[i].open(); + + + } + +} + +if (document.readyState != 'loading'){ + if (ABT_locationInfo.postType !== 'page') { new PeerReviewMetabox; } +} else { + document.addEventListener('DOMContentLoaded', () => { + if (ABT_locationInfo.postType !== 'page') { new PeerReviewMetabox } + }); +} diff --git a/inc/js/peer-review.js b/inc/js/peer-review.js deleted file mode 100644 index e0a8b478..00000000 --- a/inc/js/peer-review.js +++ /dev/null @@ -1,145 +0,0 @@ -var ABT_Frontend; -(function (ABT_Frontend) { - var Accordion = (function () { - function Accordion() { - this._headings = document.querySelectorAll('#abt_PR_boxes > h3'); - for (var _i = 0, _a = this._headings; _i < _a.length; _i++) { - var heading = _a[_i]; - var reviewContent = heading.nextSibling; - reviewContent.style.display = 'none'; - heading.addEventListener('click', this._clickHandler); - } - } - Accordion.prototype._clickHandler = function (e) { - var targetContent = e.srcElement.nextSibling; - if (targetContent.style.display != 'none') { - targetContent.style.display = 'none'; - return; - } - var accordionNodes = e.srcElement.parentElement.childNodes; - var element; - for (var _i = 0, _a = accordionNodes; _i < _a.length; _i++) { - element = _a[_i]; - if (element.tagName != 'DIV') { - continue; - } - if (element.previousSibling === e.srcElement) { - element.style.display = ''; - continue; - } - element.style.display = 'none'; - } - }; - return Accordion; - }()); - ABT_Frontend.Accordion = Accordion; - var Citations = (function () { - function Citations() { - var referenceList = this._getReferenceList(); - var citationList = document.querySelectorAll('span.cite'); - var citation; - for (var _i = 0, _a = citationList; _i < _a.length; _i++) { - citation = _a[_i]; - var citeNums = JSON.parse(citation.dataset['reflist']); - var citationHTML = citeNums.map(function (citeNum) { - citeNum--; - if (!referenceList.children[citeNum]) { - return; - } - return ("

" + - "" + - ("" + (citeNum + 1) + ". ") + - ("" + referenceList.children[citeNum].innerHTML) + - "" + - "
"); - }); - citation.dataset['citations'] = citationHTML.join(''); - if (this._isTouchDevice()) { - citation.addEventListener('touchstart', this._createTooltip.bind(this)); - } - else { - citation.addEventListener('mouseover', this._createTooltip.bind(this)); - citation.addEventListener('mouseout', this._destroyTooltip); - } - } - } - Citations.prototype._getReferenceList = function () { - var orderedLists = document.getElementsByTagName('ol'); - for (var i = (orderedLists.length - 1); i >= 0; i--) { - if (orderedLists[i].parentElement.className !== 'abt_chat_bubble') { - return orderedLists[i]; - } - } - }; - Citations.prototype._isTouchDevice = function () { - return true == ("ontouchstart" in window || window.DocumentTouch && document instanceof DocumentTouch); - }; - Citations.prototype._createTooltip = function (e) { - clearTimeout(Citations.timer); - var preExistingTooltip = document.getElementById('abt_tooltip'); - if (preExistingTooltip !== null) { - preExistingTooltip.remove(); - } - var rect = e.srcElement.getBoundingClientRect(); - var tooltip = document.createElement('div'); - tooltip.className = tooltip.id = 'abt_tooltip'; - tooltip.innerHTML = e.srcElement.getAttribute('data-citations'); - tooltip.style.visibility = 'hidden'; - var tooltipArrow = document.createElement('div'); - tooltipArrow.className = 'abt_tooltip_arrow'; - if (this._isTouchDevice()) { - var closeButton = document.createElement('div'); - var touchContainer = document.createElement('div'); - touchContainer.className = 'abt_tooltip_touch_close-container'; - closeButton.className = 'abt_tooltip_touch_close'; - touchContainer.addEventListener('touchend', function () { return tooltip.remove(); }); - tooltip.style.left = '0'; - tooltip.style.right = '0'; - tooltip.style.maxWidth = '90%'; - touchContainer.appendChild(closeButton); - tooltip.appendChild(touchContainer); - document.body.appendChild(tooltip); - tooltip.appendChild(tooltipArrow); - tooltipArrow.style.left = "calc(" + rect.left + "px - 5% + " + ((rect.right - rect.left) / 2) + "px - 3px)"; - } - else { - tooltipArrow.style.left = '50%'; - tooltip.appendChild(tooltipArrow); - document.body.appendChild(tooltip); - tooltip.style.marginRight = '10px'; - tooltip.style.maxWidth = (500 > ((rect.left * 2) + ((rect.right - rect.left) / 2))) ? (rect.left * 2) + 'px' : '500px'; - tooltip.style.left = rect.left + ((rect.right - rect.left) / 2) - (tooltip.clientWidth / 2) + 'px'; - tooltip.addEventListener('mouseover', function () { return clearTimeout(Citations.timer); }); - tooltip.addEventListener('mouseout', this._destroyTooltip); - } - if ((rect.top - tooltip.offsetHeight) < 0) { - tooltip.style.top = (rect.bottom + window.scrollY + 5) + 'px'; - tooltipArrow.style.top = '-15px'; - tooltipArrow.style.borderColor = 'transparent transparent #fff'; - } - else { - tooltip.style.top = (rect.top + window.scrollY - tooltip.offsetHeight - 5) + 'px'; - tooltipArrow.style.bottom = '-15px'; - tooltipArrow.style.borderColor = '#fff transparent transparent'; - } - tooltip.style.visibility = ''; - }; - Citations.prototype._destroyTooltip = function () { - Citations.timer = setTimeout(function () { - document.getElementById('abt_tooltip').remove(); - }, 200); - }; - return Citations; - }()); - ABT_Frontend.Citations = Citations; -})(ABT_Frontend || (ABT_Frontend = {})); -if (document.readyState != 'loading') { - frontendJS(); -} -else { - document.addEventListener('DOMContentLoaded', frontendJS); -} -function frontendJS() { - new ABT_Frontend.Citations(); - new ABT_Frontend.Accordion(); -} diff --git a/inc/js/tinymce-buttons.js b/inc/js/tinymce-buttons.js deleted file mode 100644 index 331b85c8..00000000 --- a/inc/js/tinymce-buttons.js +++ /dev/null @@ -1,456 +0,0 @@ -(function() { - - tinymce.PluginManager.add('abt_ref_id_parser_mce_button', function(editor, url) { - editor.addButton('abt_ref_id_parser_mce_button', { - type: 'menubutton', - image: url + '/../images/book.png', - title: "Academic Blogger's Toolkit", - icon: true, - menu: [{ - text: 'Bibliography Tools', - menu: [ - // Inline Citation Menu Item - { - text: 'Inline Citation', - onclick: function() { - editor.windowManager.open({ - title: 'Insert Citation', - width: 600, - height: 58, - body: [{ - type: 'textbox', - name: 'citation_number', - label: 'Citation Number', - value: '' - }, { - type: 'container', - html: 'Protip: Use the keyboard shortcut to access this menu
PC/Linux: (Ctrl+Alt+C) | Mac: (Cmd+Alt+C)' - }], - - onsubmit: function(e) { - editor.insertContent( - '[cite num="' + e.data.citation_number + '"]' - ); - } - }); - } - }, - // Separator - { - text: '-' - }, - // Single Reference Menu Item - { - text: 'Formatted Reference', - onclick: function() { - editor.windowManager.open({ - title: 'Insert Formatted Reference', - width: 600, - height: 125, - body: [{ - type: 'textbox', - name: 'ref_id_number', - label: 'PMID', - value: '' - }, { - type: 'listbox', - label: 'Citation Format', - name: 'ref_id_citation_type', - 'values': [{ - text: 'American Medical Association (AMA)', - value: 'AMA' - }, { - text: 'American Psychological Association (APA)', - value: 'APA' - }] - - }, { - type: 'checkbox', - name: 'ref_id_include_link', - label: 'Include link to PubMed?' - }, { - type: 'container', - html: 'Protip: Use the keyboard shortcut to access this menu
PC/Linux: (Ctrl+Alt+R) | Mac: (Cmd+Alt+R)' - }], - onsubmit: function(e) { - - editor.setProgressState(1); - var PMID = e.data.ref_id_number; - var citationFormat = e.data.ref_id_citation_type; - var includePubmedLink = e.data.ref_id_include_link; - - var request = new XMLHttpRequest(); - request.open('GET', 'http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=' + PMID + '&version=2.0&retmode=json', true); - request.onload = function() { - if (request.readyState === 4) { - if (request.status === 200) { - output = parseRequestData(JSON.parse(request.responseText), PMID, citationFormat, includePubmedLink); - editor.insertContent(output); - editor.setProgressState(0); - } else { - alert('ERROR: PMID not recognized.'); - editor.setProgressState(0); - return; - } - } - }; - request.send(null); - - } - }); - } - } - ] - }, { - text: 'Tracked Link', - onclick: function() { - var user_selection = tinyMCE.activeEditor.selection.getContent({ - format: 'text' - }); - editor.windowManager.open({ - title: 'Insert Tracked Link', - width: 600, - height: 160, - buttons: [{ - text: 'Insert', - onclick: 'submit' - }], - body: [{ - type: 'textbox', - name: 'tracked_url', - label: 'URL', - value: '' - }, { - type: 'textbox', - name: 'tracked_title', - label: 'Link Text', - value: user_selection - }, { - type: 'textbox', - name: 'tracked_tag', - label: 'Custom Tag ID', - tooltip: 'Don\'t forget to create matching tag in Google Tag Manager!', - value: '' - }, { - type: 'checkbox', - name: 'tracked_new_window', - label: 'Open link in a new window/tab' - }, ], - onsubmit: function(e) { - var trackedUrl = e.data.tracked_url; - var trackedTitle = e.data.tracked_title; - var trackedTag = e.data.tracked_tag; - var trackedLink; - - - if (e.data.tracked_new_window) { - trackedLink = '' + trackedTitle + ''; - } else { - trackedLink = '' + trackedTitle + ''; - } - - editor.execCommand('mceInsertContent', false, trackedLink); - } - }); - } - }, { - text: '-' - }, { - text: 'Request More Tools', - onclick: function() { - editor.windowManager.open({ - title: 'Request More Tools', - body: [ - - { - type: 'label', - text: "Have a feature or tool in mind that isn't available? Visit the link below to send a feature request. We'll do our best to make it happen." - }, { - type: 'button', - text: 'Send us your thoughts!', - onclick: function() { - window.open('https://github.com/dsifford/academic-bloggers-toolkit/issues', '_blank'); - }, - } - - ], - onsubit: function() { - return; - } - }); - } - } - - ] - }); - editor.addShortcut('meta+alt+r', 'Insert Formatted Reference', function() { - editor.windowManager.open({ - title: 'Insert Formatted Reference', - width: 600, - height: 125, - body: [{ - type: 'textbox', - name: 'ref_id_number', - label: 'PMID', - value: '' - }, { - type: 'listbox', - label: 'Citation Format', - name: 'ref_id_citation_type', - 'values': [{ - text: 'American Medical Association (AMA)', - value: 'AMA' - }, { - text: 'American Psychological Association (APA)', - value: 'APA' - }] - - }, { - type: 'checkbox', - name: 'ref_id_include_link', - label: 'Include link to PubMed?' - }], - onsubmit: function(e) { - - editor.setProgressState(1); - var PMID = e.data.ref_id_number; - var citationFormat = e.data.ref_id_citation_type; - var includePubmedLink = e.data.ref_id_include_link; - - var request = new XMLHttpRequest(); - request.open('GET', 'http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=' + PMID + '&version=2.0&retmode=json', true); - request.onload = function() { - if (request.readyState === 4) { - if (request.status === 200) { - output = parseRequestData(JSON.parse(request.responseText), PMID, citationFormat, includePubmedLink); - editor.insertContent(output); - editor.setProgressState(0); - } else { - alert('ERROR: PMID not recognized.'); - editor.setProgressState(0); - return; - } - } - }; - request.send(null); - - } - }); - }); - - editor.addShortcut('meta+alt+c', 'Insert Inline Citation', function() { - editor.windowManager.open({ - title: 'Insert Citation', - width: 600, - height: 58, - body: [{ - type: 'textbox', - name: 'citation_number', - label: 'Citation Number', - value: '' - }], - onsubmit: function(e) { - editor.insertContent( - '[cite num="' + e.data.citation_number + '"]' - ); - } - }); - }); - - }); - - function parseRequestData(data, PMID, citationFormat, includePubmedLink) { - - var authorsRaw = data.result[PMID].authors; - var title = data.result[PMID].title; - var journalName = data.result[PMID].source; - var pubYear = data.result[PMID].pubdate.substr(0, 4); - var volume = data.result[PMID].volume; - var issue = data.result[PMID].issue; - var pages = data.result[PMID].pages; - - var authors = ''; - var output, i; - - if (citationFormat === 'AMA') { - - /** - * AUTHOR PARSING - */ - - // 0 AUTHORS - if (authorsRaw.length === 0) { - alert('ERROR: No authors were found for this PMID.\n\nPlease double-check PMID or insert reference manually.'); - } - // 1 AUTHOR - else if (authorsRaw.length === 1) { - authors = data.result[PMID].authors[0].name; - } - // 2 - 6 AUTHORS - else if (authorsRaw.length > 1 && authorsRaw.length < 7) { - - for (i = 0; i < authorsRaw.length - 1; i++) { - authors += authorsRaw[i].name + ', '; - } - authors += authorsRaw[authorsRaw.length - 1].name + '. '; - } - // >7 AUTHORS - else { - for (i = 0; i < 3; i++) { - authors += authorsRaw[i].name + ', '; - } - authors += 'et al. '; - } - - // NO VOLUME NUMBER - if (volume === '') { - output = authors + ' ' + title + ' ' + journalName + '. ' + pubYear + '; ' + volume + ': ' + pages + '.'; - } - // NO ISSUE NUMBER - else if (issue === '' || issue === undefined) { - output = authors + ' ' + title + ' ' + journalName + '. ' + pubYear + '; ' + volume + ': ' + pages + '.'; - } else { - output = authors + ' ' + title + ' ' + journalName + '. ' + pubYear + '; ' + volume + '(' + issue + '): ' + pages + '.'; - } - - - } else if (citationFormat === 'APA') { - - /** - * AUTHOR PARSING - */ - - // 0 AUTHORS - if (authorsRaw.length === 0) { - alert('ERROR: No authors were found for this PMID.\n\nPlease double-check PMID or insert reference manually.'); - } - // 1 AUTHOR - else if (authorsRaw.length === 1) { - - // Check to see if both initials are listed - if ((/( \w\w)/g).test(data.result[PMID].authors[0].name)) { - authors += data.result[PMID].authors[0].name.substring(0, data.result[PMID].authors[0].name.length - 3) + ', ' + - data.result[PMID].authors[0].name.substring(data.result[PMID].authors[0].name.length - 2, data.result[PMID].authors[0].name.length - 1) + '. ' + - data.result[PMID].authors[0].name.substring(data.result[PMID].authors[0].name.length - 1) + '. '; - } else { - authors += data.result[PMID].authors[0].name.substring(0, data.result[PMID].authors[0].name.length - 2) + ', ' + - data.result[PMID].authors[0].name.substring(data.result[PMID].authors[0].name.length - 1) + '. '; - } - - } - // 2 Authors - else if (authorsRaw.length === 2) { - - if ((/( \w\w)/g).test(data.result[PMID].authors[0].name)) { - - authors += data.result[PMID].authors[0].name.substring(0, data.result[PMID].authors[0].name.length - 3) + ', ' + - data.result[PMID].authors[0].name.substring(data.result[PMID].authors[0].name.length - 2, data.result[PMID].authors[0].name.length - 1) + '. ' + - data.result[PMID].authors[0].name.substring(data.result[PMID].authors[0].name.length - 1) + '., & '; - - } else { - - authors += data.result[PMID].authors[0].name.substring(0, data.result[PMID].authors[0].name.length - 2) + ', ' + - data.result[PMID].authors[0].name.substring(data.result[PMID].authors[0].name.length - 1) + '., & '; - - } - - if ((/( \w\w)/g).test(data.result[PMID].authors[1].name)) { - - authors += data.result[PMID].authors[1].name.substring(0, data.result[PMID].authors[1].name.length - 3) + ', ' + - data.result[PMID].authors[1].name.substring(data.result[PMID].authors[1].name.length - 2, data.result[PMID].authors[1].name.length - 1) + '. ' + - data.result[PMID].authors[1].name.substring(data.result[PMID].authors[1].name.length - 1) + '. '; - - } else { - - authors += data.result[PMID].authors[1].name.substring(0, data.result[PMID].authors[1].name.length - 2) + ', ' + - data.result[PMID].authors[1].name.substring(data.result[PMID].authors[1].name.length - 1) + '. '; - - } - - } - // 3-7 AUTHORS - else if (authorsRaw.length > 2 && authorsRaw.length < 8) { - - for (i = 0; i < authorsRaw.length - 1; i++) { - - if ((/( \w\w)/g).test(data.result[PMID].authors[i].name)) { - - authors += data.result[PMID].authors[i].name.substring(0, data.result[PMID].authors[i].name.length - 3) + ', ' + - data.result[PMID].authors[i].name.substring(data.result[PMID].authors[i].name.length - 2, data.result[PMID].authors[i].name.length - 1) + '. ' + - data.result[PMID].authors[i].name.substring(data.result[PMID].authors[i].name.length - 1) + '., '; - - } else { - - authors += data.result[PMID].authors[i].name.substring(0, data.result[PMID].authors[i].name.length - 2) + ', ' + - data.result[PMID].authors[i].name.substring(data.result[PMID].authors[i].name.length - 1) + '., '; - - } - - } - - if ((/( \w\w)/g).test(data.result[PMID].lastauthor)) { - - authors += '& ' + data.result[PMID].lastauthor.substring(0, data.result[PMID].lastauthor.length - 3) + ', ' + - data.result[PMID].lastauthor.substring(data.result[PMID].lastauthor.length - 2, data.result[PMID].lastauthor.length - 1) + '. ' + - data.result[PMID].lastauthor.substring(data.result[PMID].lastauthor.length - 1) + '. '; - - } else { - - authors += '& ' + data.result[PMID].lastauthor.substring(0, data.result[PMID].lastauthor.length - 2) + ', ' + - data.result[PMID].lastauthor.substring(data.result[PMID].lastauthor.length - 1) + '. '; - - } - - } - // >7 AUTHORS - else { - - for (i = 0; i < 6; i++) { - - if ((/( \w\w)/g).test(data.result[PMID].authors[i].name)) { - - authors += data.result[PMID].authors[i].name.substring(0, data.result[PMID].authors[i].name.length - 3) + ', ' + - data.result[PMID].authors[i].name.substring(data.result[PMID].authors[i].name.length - 2, data.result[PMID].authors[i].name.length - 1) + '. ' + - data.result[PMID].authors[i].name.substring(data.result[PMID].authors[i].name.length - 1) + '., '; - - } else { - - authors += data.result[PMID].authors[i].name.substring(0, data.result[PMID].authors[i].name.length - 2) + ', ' + - data.result[PMID].authors[i].name.substring(data.result[PMID].authors[i].name.length - 1) + '., '; - - } - - } - - if ((/( \w\w)/g).test(data.result[PMID].lastauthor)) { - - authors += '. . . ' + data.result[PMID].lastauthor.substring(0, data.result[PMID].lastauthor.length - 3) + ', ' + - data.result[PMID].lastauthor.substring(data.result[PMID].lastauthor.length - 2, data.result[PMID].lastauthor.length - 1) + '. ' + - data.result[PMID].lastauthor.substring(data.result[PMID].lastauthor.length - 1) + '. '; - - } else { - - authors += '. . . ' + data.result[PMID].lastauthor.substring(0, data.result[PMID].lastauthor.length - 2) + ', ' + - data.result[PMID].lastauthor.substring(data.result[PMID].lastauthor.length - 1) + '. '; - - } - - - } - - output = authors + '(' + pubYear + '). ' + title + ' ' + journalName + ', ' + (volume !== '' ? volume : '') + (issue !== '' ? '(' + issue + '), ' : '') + pages + '.'; - - } - - // INCLUDE LINK TO PUBMED IF CHECKBOX IS CHECKED - if (includePubmedLink) { - output += ' PMID: ' + PMID + ''; - } - - return output; - - - } - -}) -(); diff --git a/inc/js/tinymce-entrypoint.ts b/inc/js/tinymce-entrypoint.ts new file mode 100644 index 00000000..55dc8134 --- /dev/null +++ b/inc/js/tinymce-entrypoint.ts @@ -0,0 +1,231 @@ +import Dispatcher from './utils/Dispatcher'; +import { parseInlineCitationString } from './utils/HelperFunctions'; + +declare var tinyMCE, ABT_locationInfo + +tinyMCE.PluginManager.add('abt_main_menu', (editor, url: string) => { + + //================================================== + // MAIN BUTTON + //================================================== + + let ABT_Button: any = { + id: 'abt_menubutton', + type: 'menubutton', + icon: 'abt_menu dashicons-welcome-learn-more', + title: 'Academic Blogger\'s Toolkit', + menu: [], + }; + + //================================================== + // BUTTON FUNCTIONS + //================================================== + + let openInlineCitationWindow = () => { + editor.windowManager.open({ + title: 'Inline Citation', + url: ABT_locationInfo.tinymceViewsURL + 'citation-window.html', + width: 400, + height: 85, + onClose: (e) => { + if (!e.target.params.data) { return; } + editor.insertContent( + '[cite num="' + parseInlineCitationString(e.target.params.data) + '"]' + ); + } + }); + } + + let openFormattedReferenceWindow = () => { + editor.windowManager.open({ + title: 'Insert Formatted Reference', + url: ABT_locationInfo.tinymceViewsURL + 'reference-window.html', + width: 600, + height: 10, + params: { + baseUrl: ABT_locationInfo.tinymceViewsURL, + preferredStyle: ABT_locationInfo.preferredCitationStyle, + }, + onclose: (e: any) => { + + // If the user presses the exit button, return. + if (Object.keys(e.target.params).length === 0) { + return; + } + + editor.setProgressState(1); + let payload: ReferenceFormData = e.target.params.data; + let refparser = new Dispatcher(payload, editor); + + if (payload.addManually === true) { + refparser.fromManualInput(payload); + return; + } + + refparser.fromPMID(); + }, + }); + }; + + let generateSmartBib = function() { + let dom: HTMLDocument = editor.dom.doc; + let existingSmartBib: HTMLOListElement = dom.getElementById('abt-smart-bib'); + + if (!existingSmartBib) { + let smartBib: HTMLOListElement = dom.createElement('OL'); + let horizontalRule: HTMLHRElement = dom.createElement('HR'); + smartBib.id = 'abt-smart-bib'; + horizontalRule.className = 'abt_editor-only'; + let comment = dom.createComment(`Smart Bibliography Generated By Academic Blogger's Toolkit`); + dom.body.appendChild(comment); + dom.body.appendChild(horizontalRule); + dom.body.appendChild(smartBib); + this.state.set('disabled', true); + } + + return; + } + + + //================================================== + // MENU ITEMS + //================================================== + + let separator: TinyMCEMenuItem = { text: '-' }; + + + let bibToolsMenu: TinyMCEMenuItem = { + text: 'Other Tools', + menu: [], + }; + + + let inlineCitation: TinyMCEMenuItem = { + text: 'Inline Citation', + onclick: openInlineCitationWindow, + } + editor.addShortcut('meta+alt+c', 'Insert Inline Citation', openInlineCitationWindow); + + let formattedReference: TinyMCEMenuItem = { + text: 'Formatted Reference', + onclick: openFormattedReferenceWindow, + } + editor.addShortcut('meta+alt+r', 'Insert Formatted Reference', openFormattedReferenceWindow); + + + let smartBib: TinyMCEMenuItem = { + text: 'Generate Smart Bibliography', + id: 'smartbib', + onclick: generateSmartBib, + disabled: false, + } + + + /** NOTE: THIS WILL BE DEPRECIATED NEXT RELEASE! */ + let trackedLink: TinyMCEMenuItem = { + text: 'Tracked Link', + onclick: () => { + + let user_selection = tinyMCE.activeEditor.selection.getContent({format: 'text'}); + + editor.windowManager.open({ + title: 'Insert Tracked Link === Depreciating Next Release! ===', + width: 600, + height: 160, + buttons: [{ + text: 'Insert', + onclick: 'submit' + }], + body: [ + { + type: 'textbox', + name: 'tracked_url', + label: 'URL', + value: '' + }, + { + type: 'textbox', + name: 'tracked_title', + label: 'Link Text', + value: user_selection + }, + { + type: 'textbox', + name: 'tracked_tag', + label: 'Custom Tag ID', + tooltip: 'Don\'t forget to create matching tag in Google Tag Manager!', + value: '' + }, + { + type: 'checkbox', + name: 'tracked_new_window', + label: 'Open link in a new window/tab' + }, + ], + onsubmit: (e) => { + let trackedUrl = e.data.tracked_url; + let trackedTitle = e.data.tracked_title; + let trackedTag = e.data.tracked_tag; + let trackedLink = `${trackedTitle}`; + + editor.execCommand('mceInsertContent', false, trackedLink); + + } + }); + } + } + // End Tracked Link Menu Item + + let requestTools: TinyMCEMenuItem = { + text: 'Request More Tools', + onclick: () => { + editor.windowManager.open({ + title: 'Request More Tools', + body: [{ + type: 'container', + html: + `
` + + `Have a feature or tool in mind that isn't available?
` + + `Open an issue on the GitHub repository and let me know!` + + `
`, + }], + buttons: [], + }); + } + } + + + let keyboardShortcuts: TinyMCEMenuItem = { + text: 'Keyboard Shortcuts', + onclick: () => { + editor.windowManager.open({ + title: 'Keyboard Shortcuts', + url: ABT_locationInfo.tinymceViewsURL + 'keyboard-shortcuts.html', + width: 400, + height: 90, + }); + } + } + + // Workaround for checking to see if a smart bib exists. + setTimeout(() => { + let dom: HTMLDocument = editor.dom.doc; + let existingSmartBib: HTMLOListElement = dom.getElementById('abt-smart-bib'); + if (existingSmartBib) { + smartBib.disabled = true; + smartBib.text = 'Smart Bibliography Generated!'; + } + }, 500); + + bibToolsMenu.menu.push(trackedLink, separator, requestTools); + ABT_Button.menu.push(smartBib, inlineCitation, formattedReference, bibToolsMenu, separator, keyboardShortcuts); + + editor.addButton('abt_main_menu', ABT_Button); + + + +}); diff --git a/inc/js/tinymce-views/citation-window.html b/inc/js/tinymce-views/citation-window.html new file mode 100644 index 00000000..d6d6c8ff --- /dev/null +++ b/inc/js/tinymce-views/citation-window.html @@ -0,0 +1,3 @@ + +
+ diff --git a/inc/js/tinymce-views/keyboard-shortcuts.html b/inc/js/tinymce-views/keyboard-shortcuts.html new file mode 100644 index 00000000..256b0ea6 --- /dev/null +++ b/inc/js/tinymce-views/keyboard-shortcuts.html @@ -0,0 +1,14 @@ + + + + + + + + + + +
ctrl+alt+cOpen Inline Citations Menu
ctrl+alt+rOpen Formatted Reference Menu
+
+ Note: If you're on Mac, substitute ctrl for cmd +
diff --git a/inc/js/tinymce-views/pubmed-window.html b/inc/js/tinymce-views/pubmed-window.html new file mode 100644 index 00000000..e92505c0 --- /dev/null +++ b/inc/js/tinymce-views/pubmed-window.html @@ -0,0 +1,3 @@ + +
+ diff --git a/inc/js/tinymce-views/reference-window.html b/inc/js/tinymce-views/reference-window.html new file mode 100644 index 00000000..09161edf --- /dev/null +++ b/inc/js/tinymce-views/reference-window.html @@ -0,0 +1,3 @@ + +
+ diff --git a/inc/js/tinymce-views/styles.scss b/inc/js/tinymce-views/styles.scss new file mode 100644 index 00000000..9b048b7b --- /dev/null +++ b/inc/js/tinymce-views/styles.scss @@ -0,0 +1,177 @@ +body { + overflow: hidden; +} + +.row { + margin-top: 10px; + margin-bottom: 10px; +} + +.manual-input-table { + width: 100%; + input { + width: 100%; + } +} + +.btn { + background: #f7f7f7; + border-color: #ccc; + border-radius: 3px; + border-style: solid; + border-width: 1px; + box-shadow: 0 1px 0 #ccc; + box-sizing: border-box; + color: #555; + cursor: pointer; + display: inline-block; + font-size: 13px; + height: 30px; + line-height: 31px; + margin: 2px 0; + padding: 0 12px 2px; + vertical-align: top; + white-space: nowrap; + -webkit-appearance: none; + + &:hover { + background: #fafafa; + border-color: #999; + color: #23282d; + } + + &:active { + background: #eee; + border-color: #999; + box-shadow: inset 0 2px 5px -3px rgba(0,0,0,.5); + transform: translateY(1px); + outline: 0; + } + + &:disabled { + color: #a0a5aa; + border-color: #ddd; + background: #f7f7f7; + box-shadow: none; + text-shadow: 0 1px 0 #fff; + cursor: default; + transform: none; + } +} + +.submit-btn { + @extend .btn; + background: #0085ba; + border-color: #0073aa #006799 #006799; + box-shadow: 0 1px 0 #006799; + color: #fff; + text-decoration: none; + text-shadow: 0 -1px 1px #006799,1px 0 1px #006799,0 1px 1px #006799,-1px 0 1px #006799; + + &:hover { + background: #008ec2; + border-color: #006799; + color: #fff + } + + &:active { + background: #0073aa; + border-color: #006799; + box-shadow: inset 0 2px 0 #006799; + vertical-align: top; + } + + &:disabled { + background: #008ec2; + color: #66c6e4; + border-color: #007cb2; + text-shadow: 0 -1px 0 rgba(0,0,0,.1); + cursor: default; + } +} + +input, select { + border: 1px solid #ddd; + box-shadow: inset 0 1px 2px rgba(0,0,0,.07); + background-color: #fff; + color: #32373c; + outline: 0; + height: 31px; + transition: 50ms border-color ease-in-out; + margin: 1px; + padding: 5px; + font-size: 1em; + + &:focus { + border-color: #5b9dd9; + box-shadow: 0 0 2px rgba(30, 140, 190, .8); + &:invalid { + box-shadow: 0 0 2px rgba(204,0,0,.8); + border-color: #dc3232!important; + } + } + + &[type="checkbox"] { + height: 18px; + width: 18px; + vertical-align: middle; + } + + &:invalid { + background: #f6c9cc; + color: #dc3232; + border: solid #E58383 1px; + } + +} + +select { + padding: 2px; + vertical-align: middle; + margin: 1px; + font-size: 1em; +} + +a { + color: #0073aa; + + &:hover { + color: #00a0d2; + } +} + +kbd { + font-family: monospace; + padding: 2px 7px 3px; + font-weight: 400; + margin: 0; + font-size: 15px; + background: #eaeaea; + background: rgba(0,0,0,.08) +} + +.cite-row { + padding: 5px; + font-size: 0.8em; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.even { + background: #fafafa; + border: 1px solid #ccc; + border-left: none; + border-right: none; + } + + &:hover { + cursor: pointer; + } +} + +.cite-selected { + background: rgba(0, 133, 186, 0.5)!important; +} diff --git a/inc/js/utils/Dispatcher.ts b/inc/js/utils/Dispatcher.ts new file mode 100644 index 00000000..13e70103 --- /dev/null +++ b/inc/js/utils/Dispatcher.ts @@ -0,0 +1,205 @@ +import { AMA, APA } from './Parsers'; +import './PrototypeFunctions'; + +export default class Dispatcher { + public citationFormat: string; + public includeLink: boolean; + public attachInline: boolean; + public manualCitationType: string; + public PMIDquery: string; + public editor: any; + public smartBib: HTMLOListElement|boolean + + constructor(data: ReferenceFormData, editor: Object) { + this.citationFormat = data.citationFormat; + this.PMIDquery = data.pmidList !== '' + ? data.pmidList.replace(/\s/g, '') + : ''; + this.manualCitationType = data.manualData.type; + this.includeLink = data.includeLink; + this.attachInline = data.attachInline; + this.editor = editor; + let smartBib = (this.editor.dom.doc as HTMLDocument) + .getElementById('abt-smart-bib') as HTMLOListElement; + this.smartBib = smartBib || false; + } + + public fromPMID(): void { + let requestURL = `http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${this.PMIDquery}&version=2.0&retmode=json`; + let request = new XMLHttpRequest(); + request.open('GET', requestURL, true); + request.addEventListener('load', this._parsePMID.bind(this)); + request.send(null); + } + + public fromManualInput(data: ReferenceFormData): void { + let cleanedData: ReferenceObj; + let type = this.manualCitationType; + + // Reformat name to + let authors: Author[] = data.manualData.authors.map((author: any) => { + let name = this._prepareName(author); + return { name }; + }); + + let meta: JournalMeta|BookMeta|WebsiteMeta = data.manualData.meta[type]; + + let title: string = meta.title.toTitleCase(); + let source: string = meta.source; + let pubdate: string = meta.pubdate; + let lastauthor: string = data.manualData.authors.length > 0 + ? this._prepareName(data.manualData.authors[data.manualData.authors.length - 1]) + : ''; + + let volume: string; + let issue: string; + let pages: string; + + let url: string; + let accessdate: string; + let updated: string; + + let location: string; + let chapter: string; + let edition: string; + + switch (type) { + case 'journal': + volume = (meta as JournalMeta).volume; + issue = (meta as JournalMeta).issue; + pages = (meta as JournalMeta).pages; + break; + case 'website': + url = (meta as WebsiteMeta).url; + accessdate = (meta as WebsiteMeta).accessed; + updated = (meta as WebsiteMeta).updated; + break; + case 'book': + pages = (meta as BookMeta).pages; + location = (meta as BookMeta).location; + chapter = (meta as BookMeta).chapter; + edition = (meta as BookMeta).edition; + break; + } + + cleanedData = { + authors, + title, + source, + pubdate, + volume, + issue, + pages, + lastauthor, + url, + accessdate, + updated, + location, + chapter, + edition, + } + + let payload: string[]|Error; + switch (this.citationFormat) { + case 'ama': + let ama = new AMA(this.includeLink, this.manualCitationType); + payload = ama.parse([cleanedData]); + break; + case 'apa': + let apa = new APA(this.includeLink, this.manualCitationType);; + payload = apa.parse([cleanedData]); + break; + default: + this.editor.windowManager.alert('An error occurred while trying to parse the citation'); + this.editor.setProgressState(0); + return; + } + + this._deliverContent(payload); + + } + + private _prepareName(author: Author): string { + return( author.lastname[0].toUpperCase() + + author.lastname.substring(1, author.lastname.length) + ' ' + + author.firstname[0].toUpperCase() + author.middleinitial.toUpperCase() + ) + } + + + private _parsePMID(e: Event): void { + let req = e.target; + + // Handle bad request + if (req.readyState !== 4 || req.status !== 200) { + this.editor.windowManager.alert('Your request could not be processed. Please try again.'); + this.editor.setProgressState(0); + return; + } + + let res = JSON.parse(req.responseText); + + // Handle response errors + if (res.error) { + let badPMID = res.error.match(/uid (\S+)/)[1]; + let badIndex = this.PMIDquery.split(',').indexOf(badPMID); + this.editor.windowManager.alert( + `PMID "${badPMID}" at index #${badIndex + 1} failed to process. Double check your list!` + ); + } + + let payload: string[]|Error; + switch (this.citationFormat) { + case 'ama': + let ama = new AMA(this.includeLink, this.citationFormat); + payload = ama.parse(res.result); + break; + case 'apa': + let apa = new APA(this.includeLink, this.citationFormat); + payload = apa.parse(res.result); + break; + default: + this.editor.windowManager.alert('An error occurred while trying to parse the citation'); + this.editor.setProgressState(0); + return; + } + + this._deliverContent(payload); + + } + + private _deliverContent(payload: string[]|Error): void { + if ((payload as Error).name === 'Error') { + this.editor.windowManager.alert((payload as Error).message); + this.editor.setProgressState(0); + return; + } + + if (this.smartBib) { + let beforeLength: number = (this.smartBib as HTMLOListElement).children.length + 1; + for (let key in (payload as string[])) { + let listItem = (this.editor.dom.doc as HTMLDocument).createElement('LI'); + listItem.innerHTML = payload[key]; + (this.smartBib as HTMLOListElement).appendChild(listItem); + } + if (this.attachInline) { + let afterLength: number = (this.smartBib as HTMLOListElement).children.length; + this.editor.insertContent(`[cite num="${beforeLength}${afterLength > beforeLength ? '-' + afterLength : ''}"]`); + } + this.editor.setProgressState(0); + return; + } + + if ((payload as string[]).length === 1) { + this.editor.insertContent((payload as string[]).join()); + this.editor.setProgressState(0); + return; + } + + let orderedList: string = + '
    ' + (payload as string[]).map((ref: string) => `
  1. ${ref}
  2. `).join('') + '
'; + + this.editor.insertContent(orderedList); + this.editor.setProgressState(0); + } +} diff --git a/inc/js/utils/HelperFunctions.ts b/inc/js/utils/HelperFunctions.ts new file mode 100644 index 00000000..8ebca34b --- /dev/null +++ b/inc/js/utils/HelperFunctions.ts @@ -0,0 +1,68 @@ + + +/** + * Function that takes a sorted array of integers as input and returns + * an inline citation string representation of the numbers. + * + * Example: [1,3,4,5,10] => "1,3-5,10" + * + * @param {number[]} numArr Sorted array of integers. + * @returns {string} A formatted inline citation string. + */ +export function parseInlineCitationString(numArr: number[]): string { + if (numArr.length === 0) { return ''; } + + let output: string = numArr[0].toString(); + + for (let i = 1; i < numArr.length; i++) { + switch (output[output.length - 1]) { + case ',': + output += numArr[i]; + break; + case '-': + if (numArr[i+1] === numArr[i] + 1) { break; } + if (i === numArr.length - 1) { output += numArr[i]; break;} + output += numArr[i] + ','; + break; + default: + let lastNum = parseInt(output.split(',')[output.split(',').length - 1]); + if (lastNum === numArr[i] - 1 && numArr[i + 1] === numArr[i] + 1) { + output += '-'; + break; + } + output += ',' + numArr[i]; + } + } + + return output; +} + +/** + * Function that takes an inline citation string and returns an array of + * integers for that string. + * + * Example: "1-3,6,8-10" => [1,2,3,6,8,9,10] + * + * @param {string} input An inline citation string. + * @returns {number[]} An array of integers that represent the input string. + */ +export function parseCitationNumArray(input: string): number[] { + let x = input.split(','); + let output: number[] = []; + + if (x.length === 0 || (x.length === 1 && x[0] === '')) { return []; } + + for (let i = 0; i < x.length; i++) { + switch (x[i].match('-')) { + case null: + output.push(parseInt(x[i])); + break; + default: + for (let j = parseInt(x[i].split('-')[0]); j <= parseInt(x[i].split('-')[1]); j++) { + output.push(j); + } + } + } + + return output; +} diff --git a/inc/js/utils/Modal.ts b/inc/js/utils/Modal.ts new file mode 100644 index 00000000..bafd12a5 --- /dev/null +++ b/inc/js/utils/Modal.ts @@ -0,0 +1,40 @@ +export default class Modal { + + public title: string + public outer: HTMLElement + public inner: HTMLElement + public mceReset: HTMLElement + public mainRect: HTMLElement + public initialSize: { + outer: number + inner: number + } + + constructor(title: string) { + this.title = title; + this._getModal(); + this.initialSize = { + outer: parseInt(this.outer.style.height.substr(0, this.outer.style.height.length - 2)), + inner: parseInt(this.inner.style.height.substr(0, this.inner.style.height.length - 2)), + } + } + + public resize(): void { + let height = this.mainRect.getBoundingClientRect().height; + let position = `calc(50% - ${(height + 56) / 2}px)`; + this.outer.style.height = height + 56 + 'px'; + this.outer.style.top = position; + }; + + private _getModal(): void { + let outerModalID: string = top.document.querySelector(`div.mce-floatpanel[aria-label="${this.title}"]`).id; + let innerModalID: string = `${outerModalID}-body`; + this.outer = top.document.getElementById(outerModalID); + this.inner = top.document.getElementById(innerModalID); + this.mceReset = this.outer.children[0] as HTMLElement; + this.mainRect = document.getElementById('main-container'); + this.mceReset.style.height = '100%'; + this.inner.style.height = '100%'; + } + +} diff --git a/inc/js/utils/Parsers.ts b/inc/js/utils/Parsers.ts new file mode 100644 index 00000000..ed93e1bc --- /dev/null +++ b/inc/js/utils/Parsers.ts @@ -0,0 +1,302 @@ + +export class AMA { + + private _isManual: boolean = true; + private manualCitationType: string; + private includeLink: boolean; + + constructor(includeLink?: boolean, manualCitationType?: string) { + this.includeLink = includeLink; + this.manualCitationType = manualCitationType; + } + + public parse(data: ReferencePayload): string[]|Error { + + if (data.uids) { + this._isManual = false + } + + if (this._isManual) { + return [this._fromManual(data)]; + } + + return this._fromPMID(data, data.uids); + } + + private _fromPMID(data: ReferencePayload, pmidArray: string[]): string[]|Error { + let output: string[]|Error; + try { + output = pmidArray.map((PMID: string): string => { + let ref: ReferenceObj = data[PMID]; + let year: string = ref.pubdate.substr(0, 4); + let link = this.includeLink === true + ? ` PMID: ${PMID}` + : ''; + + let authors: string|Error = this._parseAuthors(ref.authors); + if ((authors as Error).name === 'Error') { + throw authors; + } + + return `${authors} ${ref.title} ${ref.source}. ${year}; ` + + `${ref.volume === undefined || ref.volume === '' ? '' : ref.volume}` + + `${ref.issue === undefined || ref.issue === '' ? '' : '('+ref.issue+')'}:` + + `${ref.pages}.${link}`; + }); + } catch(e) { + return e; + } + return output; + } + + private _fromManual(data: ReferencePayload): string { + + let payload: string; + switch (this.manualCitationType) { + case 'journal': + payload = this._parseJournal(data); + break; + case 'website': + payload = this._parseWebsite(data); + break; + case 'book': + payload = this._parseBook(data); + break; + } + + return payload; + } + + private _parseAuthors(authorArr: Author[]): string|Error { + let authors: string = ''; + switch (authorArr.length) { + case 0: + if (this._isManual === true) { break; } + return new Error(`No authors were found for given reference`); + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + authors = authorArr.map((author: Author) => author.name).join(', ') + '.'; + break; + default: + for (let i = 0; i < 3; i++) { authors+= authorArr[i].name + ', ' }; + authors += 'et al.'; + } + return authors; + } + + private _parseJournal(data: ReferencePayload): string { + let authors = this._parseAuthors(data[0].authors); + let year = (new Date(data[0].pubdate).getFullYear() + 1).toString(); + let source = data[0].source.toTitleCase(); + let issue = `(${data[0].issue})` || ''; + let volume = data[0].volume || ''; + + return `${authors} ${data[0].title}. ${source}. ${year}; ` + + `${volume}${issue}:${data[0].pages}.`; + } + + private _parseWebsite(data: ReferencePayload): string { + let authors = data[0].authors.length > 0 + ? this._parseAuthors(data[0].authors) + ' ' + : ''; + let pubdate: string = `Published ${new Date(data[0].pubdate).toLocaleDateString('en-us', {month: 'long', year: 'numeric'})}. `; + let updated: string = data[0].updated !== '' + ? `Updated ${new Date(data[0].updated).toLocaleDateString('en-us', {month: 'long', day: 'numeric', year: 'numeric'})}. ` + : '' + let accessed: string = data[0].accessdate !== '' + ? `Accessed ${new Date(data[0].accessdate).toLocaleDateString('en-us', {month: 'long', day: 'numeric', year: 'numeric'})}. ` + : `Accessed ${new Date(Date.now()).toLocaleDateString('en-us', {month: 'long', day: 'numeric', year: 'numeric'})}`; + + return `${authors}${data[0].title}. ${data[0].source}. Available at: ` + + `${data[0].url}. ${pubdate}${updated}${accessed}`; + } + + private _parseBook(data: ReferencePayload): string { + let authors = this._parseAuthors(data[0].authors); + let title = data[0].title; + let pubLocation = data[0].location !== '' + ? `${data[0].location}:` + : ``; + let publisher = data[0].source; + let year = data[0].pubdate; + let chapter = data[0].chapter !== '' + ? ` ${data[0].chapter}. In:` + : ``; + let pages = data[0].pages !== '' + ? `: ${data[0].pages}.` + : `.`; + + return `${authors}${chapter} ${title}. ${pubLocation}${publisher}; ${year}${pages}`; + } + +} + + +export class APA { + + private _isManual: boolean = true; + private manualCitationType: string; + private includeLink: boolean; + + constructor(includeLink?: boolean, manualCitationType?: string) { + this.includeLink = includeLink; + this.manualCitationType = manualCitationType; + } + + public parse(data: ReferencePayload): string[]|Error { + let pmidArray: string[]|boolean = data.uids || false; + + if (pmidArray) { + this._isManual = false; + return this._fromPMID(data, (pmidArray as string[])); + } + + return [this._fromManual(data)]; + } + + private _fromPMID(data: ReferencePayload, pmidArray: string[]): string[]|Error { + + let output: string[]; + + try { + output = pmidArray.map((PMID: string): string => { + let ref: ReferenceObj = data[PMID]; + let year: string = ref.pubdate.substr(0, 4); + let link = this.includeLink === true + ? ` PMID: ${PMID}` + : ''; + + let authors: string|Error = this._parseAuthors(ref.authors, ref.lastauthor); + if ((authors as Error).name === 'Error') { + throw authors; + } + + return `${authors} (${year}). ${ref.title} ` + + `${ref.fulljournalname === undefined || ref.fulljournalname === '' ? ref.source : ref.fulljournalname.toTitleCase()}., ` + + `${ref.volume === undefined || ref.volume === '' ? '' : ref.volume}` + + `${ref.issue === undefined || ref.issue === '' ? '' : '('+ref.issue+')'}, ` + + `${ref.pages}.${link}`; + }); + } catch(e) { + return e; + } + + return output; + + } + + private _fromManual(data: ReferencePayload): string { + let payload: string; + switch (this.manualCitationType) { + case 'journal': + payload = this._parseJournal(data); + break; + case 'website': + payload = this._parseWebsite(data); + break; + case 'book': + payload = this._parseBook(data); + break; + } + + return payload; + } + + private _parseAuthors(authorArr: Author[], lastAuthor: string): string|Error { + let authors: string = ''; + + switch (authorArr.length) { + case 0: + if (this._isManual === true) { break; } + return new Error(`No authors were found for given reference`); + case 1: + authors = authorArr.map((author: Author) => + `${author.name.split(' ')[0]}, ` + // Last name + `${author.name.split(' ')[1].split('').join('. ')}.` // First Initial(s) + ).join(); + break; + case 2: + authors = authorArr.map((author: Author) => + `${author.name.split(' ')[0]}, ` + // Last name + `${author.name.split(' ')[1].split('').join('. ')}.` // First Initial(s) + ).join(', & '); + break; + case 3: + case 4: + case 5: + case 6: + case 7: + authors = authorArr.map((author, i, arr) => { + if (i === arr.length - 1) { + return( + `& ${author.name.split(' ')[0]}, ` + + `${author.name.split(' ')[1].split('').join('. ')}.` + ); + } + return( + `${author.name.split(' ')[0]}, ` + + `${author.name.split(' ')[1].split('').join('. ')}., ` + ); + }).join(''); + break; + default: + for (let i = 0; i < 6; i++) { + authors += + `${authorArr[i].name.split(' ')[0]}, ` + + `${authorArr[i].name.split(' ')[1].split('').join('. ')}., ` + } + authors += `. . . ` + + `${lastAuthor.split(' ')[0]}, ` + + `${lastAuthor.split(' ')[1].split('').join('. ')}.`; + break; + } + return authors; + + + } + + private _parseJournal(data: ReferencePayload): string { + let authors = this._parseAuthors(data[0].authors, data[0].lastauthor); + let year = (new Date(data[0].pubdate).getFullYear() + 1).toString(); + let source = data[0].source.toTitleCase(); + let issue = `(${data[0].issue})` || ''; + let volume = data[0].volume || ''; + + return `${authors} (${year}). ${data[0].title}. ` + + `${source}., ${volume}${issue}, ${data[0].pages}.`; + } + + private _parseWebsite(data: ReferencePayload): string { + let authors = this._parseAuthors(data[0].authors, data[0].lastauthor); + let rawDate = new Date(data[0].pubdate); + let source = data[0].source.toTitleCase(); + let date = `${rawDate.getFullYear()}, ` + + `${rawDate.toLocaleDateString('en-us', {month: 'long', day: 'numeric'})}`; + + return `${authors} (${date}). ${data[0].title}. ${source}. ` + + `Retrieved from ${data[0].url}`; + } + + private _parseBook(data: ReferencePayload): string { + let authors = this._parseAuthors(data[0].authors, data[0].lastauthor); + let year = (new Date(data[0].pubdate).getFullYear() + 1).toString(); + let pubLocation = data[0].location !== '' + ? `${data[0].location}:` + : ''; + let publisher = data[0].source; + let chapter = data[0].chapter !== '' + ? ` ${data[0].chapter}. In` + : ''; + let pages = data[0].pages !== '' + ? ` (${data[0].pages})` + : ''; + + return `${authors} (${year}).${chapter} ${data[0].title}${pages}. ` + + `${pubLocation}${publisher}.`; + } + +} diff --git a/inc/js/utils/PrototypeFunctions.ts b/inc/js/utils/PrototypeFunctions.ts new file mode 100644 index 00000000..6a780374 --- /dev/null +++ b/inc/js/utils/PrototypeFunctions.ts @@ -0,0 +1,26 @@ +export default {}; + +declare global { + interface String { + toTitleCase(): string + } +} + +String.prototype.toTitleCase = function(): string { + let smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i; + + return this.replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, function(match, index, title){ + if (index > 0 && index + match.length !== title.length && + match.search(smallWords) > -1 && title.charAt(index - 2) !== ":" && + (title.charAt(index + match.length) !== '-' || title.charAt(index - 1) === '-') && + title.charAt(index - 1).search(/[^\s-]/) < 0) { + return match.toLowerCase(); + } + + if (match.substr(1).search(/[A-Z]|\../) > -1) { + return match; + } + + return match.charAt(0).toUpperCase() + match.substr(1); + }); +}; diff --git a/inc/js/utils/PubmedAPI.ts b/inc/js/utils/PubmedAPI.ts new file mode 100644 index 00000000..2ae99f3e --- /dev/null +++ b/inc/js/utils/PubmedAPI.ts @@ -0,0 +1,68 @@ + + +export function PubmedQuery(query: string, callback: Function): void { + + let requestURL: string = `http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${encodeURI(query)}&retmode=json` + let request = new XMLHttpRequest(); + request.open('GET', requestURL, true); + request.onload = () => { + + // Handle bad request + if (request.readyState !== 4 || request.status !== 200) { + let error = new Error('Error: PubMed Query, Phase 1 - PubMed returned a non-200 status code.'); + callback(error); + return; + } + + let res = JSON.parse(request.responseText); + + // Handle response errors + if (res.error) { + let error = new Error('Error: PubMed Query, Phase 1 - Unknown response error.'); + callback(error); + return; + } + + getData(res.esearchresult.idlist.join(), callback); + + }; + request.send(null); +} + +function getData(PMIDlist: string, callback: Function): void { + + let requestURL = `http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${PMIDlist}&version=2.0&retmode=json`; + let request = new XMLHttpRequest(); + request.open('GET', requestURL, true); + request.onload = () => { + + // Handle bad request + if (request.readyState !== 4 || request.status !== 200) { + let error = new Error('Error: PubMed Query, Phase 2 - PubMed returned a non-200 status code.'); + callback(error); + return; + } + + let res = JSON.parse(request.responseText); + + // Handle response errors + if (res.error) { + let error = new Error('Error: PubMed Query, Phase 2 - Unknown response error.'); + callback(error); + return; + } + + + let iterable = []; + + for (let i in (res.result as Object)) { + if (i === 'uids') { continue; } + iterable.push(res.result[i]); + } + + callback(iterable); + + }; + request.send(null); + +} diff --git a/inc/options-page-wrapper.php b/inc/options-page-wrapper.php index f4110d5e..9134ce10 100644 --- a/inc/options-page-wrapper.php +++ b/inc/options-page-wrapper.php @@ -1,22 +1,12 @@
-

- +

-
- -
-
- - -
- -

- +

@@ -29,63 +19,71 @@ - + - - + + - + - +
Classes / IDs used in this plugin:CSS classes used in this plugin:
Anchor Links:.cite, .cite-returnInline Citations:.abt_cite
Peer Review Boxes:#abt_PR_boxes h3, .abt_PR_info, .abt_PR_headshot, .abt_chat_bubble.abt_PR_heading, .abt_PR_info, .abt_PR_headshot, .abt_chat_bubble
Citation Tooltips:.abt_tooltip.abt_tooltip, .abt_tooltip_arrow, .abt_tooltip_touch_close
- - -
- - - - -
- -

- +

- -
- - - - - -
- +
+
+

Make my tooltips a different color?

+
.abt_tooltip { +   background-color: magenta; +   border: solid lime 2px; + } + .abt_arrow_up { +   border-color: transparent transparent magenta; + } + .abt_arrow_down { +   border-color: magenta transparent transparent; + } +
+
+
+

Make my citations superscript?

+
.abt_cite { +   vertical-align: super; +   font-size: 0.8em; + } +
+
+
+

Apply style to the bibliography list?

+
.abt-smart-bib { +   vertical-align: super; +   font-size: 0.8em; +   list-style-type: upper-roman; +   padding: 20px 40px; +   border: solid; +   border-radius: 3px; + } +
+
+
- -
- - - - -
- -

- +

@@ -149,52 +147,40 @@
- -
- - -
- -
- - - -
-
-
-

- + 'Please send your feedback!', 'wp_admin_style' + ); ?>

GitHub Repository and leave a comment. I'll do my best to get it handled in a timely manner.

Comments can also be sent to me on twitter @flightmed1.", - 'wp_admin_style' - ); ?>

+ "If you experience a bug or would like to request a new feature, please visit my GitHub Repository and leave a comment. I'll do my best to get it handled in a timely manner.

Comments can also be sent to me on twitter @flightmed1.", + 'wp_admin_style' + ); ?>

+
+
+
+
+
+

+
+
+ + + +
- -
- -
- -
- -
- -
- - -
+
diff --git a/inc/peer-review.php b/inc/peer-review.php index 2127bbe4..376a6da8 100644 --- a/inc/peer-review.php +++ b/inc/peer-review.php @@ -6,7 +6,6 @@ function abt_peer_review_meta() { add_action( 'add_meta_boxes', 'abt_peer_review_meta' ); - function abt_peer_review_callback( $post ) { wp_nonce_field( basename( __file__ ), 'abt_nonce' ); @@ -48,7 +47,6 @@ function abt_peer_review_callback( $post ) { - @@ -85,7 +83,7 @@ function abt_peer_review_callback( $post ) { - +
@@ -216,6 +214,28 @@ function abt_meta_save( $post_id ) { add_action( 'save_post', 'abt_meta_save' ); +function tag_ordered_list( $content ) { + + if ( is_single() || is_page() ) { + + $smart_bib_exists = preg_match('
    ', $content); + + if (!$smart_bib_exists) { + + $lastOLPosition = strrpos($content, '' . - '
    ' . - '' . ${'author_name_' . $i} . '
    ' . - ${'author_background_' . $i} . '
    ' . - ${'author_twitter_' . $i} . + '
    ' . + '
    ' . + ( + ${'author_headshot_image_' . $i} !== '' + ? '' + : '' + ) . + '
    ' . + '
    ' . + '' . ${'author_name_' . $i} . '' . + '
    ' . + '
    ' . + '
    ' . + ${'author_background_' . $i} . + '
    ' . + ${'author_twitter_' . $i} . + '
    ' . '
    '; } @@ -300,13 +333,26 @@ function insert_the_meta( $text ) { if ( ${'reviewer_name_' . $i} != '' ) { ${'reviewer_block_' . $i} = - '

    ' . ${'peer_review_box_heading_' . $i} . '

    ' . + '

    ' . ${'peer_review_box_heading_' . $i} . '

    ' . '
    ' . '
    ' . ${'peer_review_content_' . $i} . '
    ' . - '
    ' . - '' . ${'reviewer_name_' . $i} . '
    ' . - ${'reviewer_background_' . $i} . '
    ' . - ${'reviewer_twitter_' . $i} . + '
    ' . + '
    ' . + ( + ${'reviewer_headshot_image_' . $i} !== '' + ? '' + : '' + ) . + '
    ' . + '
    ' . + '' . ${'reviewer_name_' . $i} . '' . + '
    ' . + '
    ' . + '
    ' . + ${'reviewer_background_' . $i} . + '
    ' . + ${'reviewer_twitter_' . $i} . + '
    ' . '
    ' . ( isset(${'author_block_' . $i}) ? ${'author_block_' . $i} : '' ) . '
    '; @@ -339,7 +385,7 @@ function insert_the_meta( $text ) { function abt_peer_review_js_enqueue() { - wp_register_script('peer_review', plugins_url('academic-bloggers-toolkit/inc/js/peer-review.js') ); + wp_register_script('peer_review', plugins_url('academic-bloggers-toolkit/inc/js/frontend.js') ); wp_enqueue_script( 'peer_review' ); } @@ -352,20 +398,23 @@ function abt_peer_review_js_enqueue() { /** * Loads the image management javascript */ -function abt_image_enqueue() { - global $typenow; - if( $typenow == 'post' ) { - wp_enqueue_media(); +function abt_image_enqueue( $hook ) { + + global $typenow; + + if( $hook == 'post.php' ) { + + wp_enqueue_media(); + $abt_options = get_option( 'abt_options' ); // Registers and enqueues the required javascript. - wp_register_script( 'meta-box-image', plugins_url('academic-bloggers-toolkit/inc/js/meta-box-image.js'), array( 'jquery' ) ); - wp_localize_script( 'meta-box-image', 'meta_image', - array( - 'title' => __( 'Choose or Upload an Image', 'abt-textdomain' ), - 'button' => __( 'Use this image', 'abt-textdomain' ), - ) - ); - wp_enqueue_script( 'meta-box-image' ); + wp_enqueue_script( 'abt-metaboxes', plugins_url('academic-bloggers-toolkit/inc/js/metaboxes.js') ); + wp_localize_script( 'abt-metaboxes', 'ABT_locationInfo', array( + 'jsURL' => plugins_url('academic-bloggers-toolkit/inc/js/'), + 'tinymceViewsURL' => plugins_url('academic-bloggers-toolkit/inc/js/tinymce-views/'), + 'preferredCitationStyle' => $abt_options['abt_citation_style'], + 'postType' => $typenow, + )); } } add_action( 'admin_enqueue_scripts', 'abt_image_enqueue' ); diff --git a/inc/shortcodes.php b/inc/shortcodes.php index 98588b21..7a9fd813 100644 --- a/inc/shortcodes.php +++ b/inc/shortcodes.php @@ -48,7 +48,7 @@ function inline_citation ( $atts ) { $nums = implode(', ', $nums); - return '[' . $nums . ']'; + return '[' . $nums . ']'; } add_shortcode( 'cite', 'inline_citation' ); diff --git a/inc/tinymce-init.php b/inc/tinymce-init.php index 99c24124..5b917539 100644 --- a/inc/tinymce-init.php +++ b/inc/tinymce-init.php @@ -1,38 +1,36 @@ - \ No newline at end of file + ?> diff --git a/jestPreprocessor.js b/jestPreprocessor.js new file mode 100644 index 00000000..02504a79 --- /dev/null +++ b/jestPreprocessor.js @@ -0,0 +1,19 @@ +const tsc = require('typescript'); + +module.exports = { + process(src, path) { + if (path.endsWith('.ts') || path.endsWith('.tsx')) { + return tsc.transpile( + src, + { + module: tsc.ModuleKind.CommonJS, + jsx: tsc.JsxEmit.React, + }, + path, + [] + ); + } + + return src; + }, +}; diff --git a/package.json b/package.json index f1462074..efba9027 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,48 @@ { "name": "academic-bloggers-toolkit", - "version": "2.3.0", + "version": "2.4.0", "description": "A plugin extending the functionality of WordPress for Academic Blogging.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest" }, "repository": { "type": "git", "url": "git+https://github.com/dsifford/academic-bloggers-toolkit.git" }, "author": "Derek P Sifford", - "license": "MIT", + "license": "GPL3 or later", "bugs": { "url": "https://github.com/dsifford/academic-bloggers-toolkit/issues" }, - "homepage": "https://github.com/dsifford/academic-bloggers-toolkit#readme" + "homepage": "https://github.com/dsifford/academic-bloggers-toolkit#readme", + "devDependencies": { + "babel-core": "^6.7.4", + "babel-loader": "^6.2.4", + "babel-preset-es2015": "^6.6.0", + "browser-sync": "^2.11.2", + "del": "^2.2.0", + "gulp": "^3.9.1", + "gulp-autoprefixer": "^3.1.0", + "gulp-clean-css": "^2.0.4", + "gulp-sass": "^2.2.0", + "gulp-uglify": "^1.5.3", + "jest-cli": "^0.10.0", + "react": "^0.14.8", + "react-addons-test-utils": "^0.14.8", + "react-dom": "^0.14.8", + "ts-loader": "^0.8.1", + "typescript": "^1.8.9", + "webpack": "^1.12.14", + "webpack-stream": "^3.1.0" + }, + "jest": { + "scriptPreprocessor": "jestPreprocessor.js", + "testFileExtensions": ["ts", "tsx", "js"], + "unmockedModulePathPatterns": [ + "/node_modules/react", + "/node_modules/react-dom", + "/node_modules/react-addons-test-utils" + ] + } } diff --git a/readme.txt b/readme.txt index be679da5..7e2e8e9e 100644 --- a/readme.txt +++ b/readme.txt @@ -1,43 +1,28 @@ === Academic Blogger's Toolkit === Contributors: dsifford Donate link: https://cash.me/$dsifford -Tags: academic, pmid, doi, peer-review, Google Tag Manager, citation, bibliography +Tags: academic, pmid, doi, peer-review, pubmed, citation, bibliography, reference Requires at least: 4.2.2 -Tested up to: 4.2.2 -Stable tag: 2.3.0 +Tested up to: 4.5 +Stable tag: 2.4.0 License: GPL3 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html -A WordPress plugin extending the functionality of WordPress for Academic Blogging. +A plugin extending the functionality of Wordpress for academic blogging. == Description == Academic Blogger's toolkit is an **open source** WordPress plugin providing an all-in-one solution for effective academic blogging. -= Automatically parse references on the fly using PMID = -- Option to add hyperlink to PubMed. += Feature List = +* Insert formatted references on the fly using PMID. +* Search and insert references from PubMed directly within WordPress. +* **Smart Bibliography** - Insert references to a bibliography and append inline citations without breaking focus of your writing. +* References inserted using this plugin are displayed as tooltips on hover in the post. No need to scroll down to the reference list to check the reference (unless you want to!). +* Append up to 3 formatted Peer Reviews to blog posts via a Frontend UI integrated on the post edit screen. -= Inline citations with hover tooltips showing full reference = -- **Requirements**: - - A bibliography ordered list must be present **AND** the ordered list must be last one in your blog post. -- **How to use**: - - Select `Bibliography Tools -> inline citation` from the options menu located on the editor and insert a list of one or more citation numbers from your bibliography list in the form of `1-4,7,9`. - -= Append up to 3 formatted Peer Reviews to blog posts via a Frontend UI integrated on the post edit screen = -- Input areas for the Peer Review section include... - 1. Reviewer Name - 2. Reviewer Background (optional) - 3. Reviewer Twitter Handle (optional) - 4. Peer Review - 5. Reviewer Photo (interfaces with WordPress's Media Uploader) -- Option to add Author Response to Peer Reviews. - -= Integration with Google Tag Manager = -- Google's newest [analytics powerhouse](http://www.google.com/tagmanager/)! -- Track individual link clicks to see who is interacting with or downloading your content and much more! - -= Customizable Options = -* CSS override area to adjust the look of any content that doesn't fit your site's style. += Want to learn more? = +Check out this plugin's [GitHub Repository](https://github.com/dsifford/academic-bloggers-toolkit) for more details or to ask questions. == Installation == @@ -63,6 +48,31 @@ Yikes! I'm sorry about that. Please report all issues on the Academic Blogger's == Changelog == += 2.4.0 = +* Parse multiple comma-separated PMIDs at once into an ordered list. +* Option to add references manually for Journals, Websites, or Books. +* Search PubMed from WordPress! + * References from your search are displayed in a list similar to native PubMed and, if you find one you like, click it and it'll be inserted into your post. +* Add optional "Smart Bibliography" feature which, if enabled, allows you to... + * Insert references directly to your bibliography without having to scroll down. + * Insert references and inline citations in one step. + * Choose from a visual list of references in your bibliography if you do not choose to add citations in one step. +* If Smart Bibliography not used, the last-occurring ordered list is automatically tagged with the HTML ID `abt-smart-bib` on load to allow for more reliable tooltip rendering. +* Details for nerds: + * Full rewrite; a majority of which is using React by Facebook. + * Speed improvements & resource minification. + +Documentation improvements will be added within the next few days. + +**Note:** Due to the magnitude of this update, there may be bugs that I have not encountered (although, I did test this pretty heavily). +If you run into any problems, have any questions, or experience a bug, please file an issue [here](https://github.com/dsifford/academic-bloggers-toolkit/issues). + +A special thanks to @metallikat36 for the great suggestions that led directly to the features added in this update. + += 2.3.1 = +* Fix poor rendering of tooltip close icon on mobile. +* Increase size of toucharea for tooltip close icon on mobile. + = 2.3.0 = * Tooltips on desktop and mobile given a much-needed facelift. * Tooltips now appear above or below depending on page scroll position (prevents chopping). diff --git a/tsconfig.json b/tsconfig.json index bf44b1a8..0ca2d971 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "module": "commonjs", "moduleResolution": "node", "isolatedModules": false, @@ -15,14 +15,42 @@ "preserveConstEnums": true, "suppressImplicitAnyIndexErrors": true }, + "filesGlob": [ + "**/*.ts", + "**/*.tsx", + "!node_modules/**", + "../../typings/**" + ], "exclude": [ "node_modules", "typings/browser", - "typings/browser.d.ts" + "typings/browser.d.ts", + "dist" ], - "compileOnSave": true, + "compileOnSave": false, "buildOnSave": false, "atom": { - "rewriteTsconfig": false - } + "rewriteTsconfig": true + }, + "files": [ + "inc/js/ABT.d.ts", + "inc/js/frontend.ts", + "inc/js/metaboxes.ts", + "inc/js/tinymce-entrypoint.ts", + "inc/js/utils/Dispatcher.ts", + "inc/js/utils/HelperFunctions.ts", + "inc/js/utils/Modal.ts", + "inc/js/utils/Parsers.ts", + "inc/js/utils/PrototypeFunctions.ts", + "inc/js/utils/PubmedAPI.ts", + "typings/main.d.ts", + "typings/main/ambient/jest/index.d.ts", + "typings/main/ambient/react-addons-test-utils/index.d.ts", + "typings/main/ambient/require/index.d.ts", + "typings/react-dom/index.d.ts", + "typings/react/index.d.ts", + "inc/js/components/CitationWindow.tsx", + "inc/js/components/PubmedWindow.tsx", + "inc/js/components/ReferenceWindow.tsx" + ] } diff --git a/typings.json b/typings.json new file mode 100644 index 00000000..b2c9af28 --- /dev/null +++ b/typings.json @@ -0,0 +1,12 @@ +{ + "name": "academic-bloggers-toolkit", + "version": false, + "dependencies": {}, + "ambientDependencies": { + "jest": "registry:dt/jest#0.9.0+20160319033628", + "react": "registry:dt/react#0.14.0+20160319053454", + "react-addons-test-utils": "registry:dt/react-addons-test-utils#0.14.0+20160316155526", + "react-dom": "registry:dt/react-dom#0.14.0+20160316155526", + "require": "registry:dt/require#2.1.20+20160316155526" + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..1bea1c69 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,27 @@ +module.exports = { + devtool: 'source-map', + entry: { + 'inc/js/tinymce-entrypoint': './inc/js/tinymce-entrypoint.ts', + 'inc/js/frontend': './inc/js/frontend.ts', + 'inc/js/components/ReferenceWindow': './inc/js/components/ReferenceWindow.tsx', + 'inc/js/components/CitationWindow': './inc/js/components/CitationWindow.tsx', + 'inc/js/metaboxes': './inc/js/metaboxes.ts', + 'inc/js/components/PubmedWindow': './inc/js/components/PubmedWindow.tsx', + }, + output: { + filename: '[name].js', + path: __dirname, + }, + module: { + loaders: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + loader: 'babel?presets[]=es2015!ts', + }, + ], + }, + resolve: { + extensions: ['', '.ts', '.tsx', '.js'], + }, +};