From 75785d86429ce9f6cfc98d192ed59884b5b74ec9 Mon Sep 17 00:00:00 2001 From: Derek P Sifford Date: Thu, 27 Dec 2018 20:24:29 -0500 Subject: [PATCH] add static publication list block --- package-lock.json | 64 +++-- package.json | 6 +- src/js/editor.ts | 2 + src/js/gutenberg/blocks/index.ts | 33 +++ .../blocks/static-bibliography/edit.tsx | 258 ++++++++++++++++++ .../blocks/static-bibliography/index.tsx | 81 ++++++ .../blocks/static-bibliography/save.tsx | 29 ++ .../blocks/static-bibliography/style.scss | 47 ++++ src/js/utils/data.ts | 6 + types/wordpress__components/index.d.ts | 9 + 10 files changed, 514 insertions(+), 21 deletions(-) create mode 100644 src/js/gutenberg/blocks/static-bibliography/edit.tsx create mode 100644 src/js/gutenberg/blocks/static-bibliography/index.tsx create mode 100644 src/js/gutenberg/blocks/static-bibliography/save.tsx create mode 100644 src/js/gutenberg/blocks/static-bibliography/style.scss diff --git a/package-lock.json b/package-lock.json index b1ab111f..95c2c519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4218,13 +4218,13 @@ } }, "css-loader": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.0.2.tgz", - "integrity": "sha512-28hdCb5gCuTKUA+R6KzLwgxK6pUfgvrUyMNn7avOUQYFvmc13djru28uG+NF/pRre7Odd6B/kmJErCcpFZZQpQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.0.tgz", + "integrity": "sha512-MoOu+CStsGrSt5K2OeZ89q3Snf+IkxRfAIt9aAKg4piioTrhtP1iEFPu+OVn3Ohz24FO6L+rw9UJxBILiSBw5Q==", "dev": true, "requires": { "icss-utils": "^4.0.0", - "loader-utils": "^1.0.2", + "loader-utils": "^1.2.1", "lodash": "^4.17.11", "postcss": "^7.0.6", "postcss-modules-extract-imports": "^2.0.0", @@ -4233,6 +4233,34 @@ "postcss-modules-values": "^2.0.0", "postcss-value-parser": "^3.3.0", "schema-utils": "^1.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.1.tgz", + "integrity": "sha512-3Zhx4qDqBQ9U8udWB3RMJ29nLu5a3ObNOSzk87woPvge01pi0wABowgv7F79Z4mL0DGtHRi/oOndT34EVhInoQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + } } }, "css-select": { @@ -12834,9 +12862,9 @@ } }, "snapshot-diff": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/snapshot-diff/-/snapshot-diff-0.4.1.tgz", - "integrity": "sha512-MzrvdlhOlXk8MinDrahnXGFkrf+rUWzMGVpZpmS1UNpVjxPMfCA4GJFN9WmxNF13ZrLiuzk73SsfT99tKbRPXQ==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/snapshot-diff/-/snapshot-diff-0.4.2.tgz", + "integrity": "sha512-umZa05vV/rmy9r+Rd7s7jBScYoGeZ9VoMBEkx9MumlTV5XXwBfB26y8iD8mQL23VfW1PUagciqzM21VHsQy4cw==", "dev": true, "requires": { "jest-diff": "^23.0.1", @@ -13683,31 +13711,31 @@ } }, "stylelint-scss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.4.3.tgz", - "integrity": "sha512-+D8sq5obF2ffrMQ53tcJkgNRUvjJwf239WKiibVXSKZQizczEiETHuT5sBLTXNZQ10KWD5jdpt7fKnruj5SgXA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.4.4.tgz", + "integrity": "sha512-GquwsRegF2gsVRePaUN93cYf9aJDygr03X/QRiwk9O5lOe7QZHlM4Vzrb8JAu+pZ0xodPRpN6W259yA6ApM3WA==", "dev": true, "requires": { "lodash": "^4.17.11", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^4.0.0", + "postcss-selector-parser": "^5.0.0", "postcss-value-parser": "^3.3.1" }, "dependencies": { "cssesc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-1.0.1.tgz", - "integrity": "sha512-S2hzrpWvE6G/rW7i7IxJfWBYn27QWfOIncUW++8Rbo1VB5zsJDSVPcnI+Q8z7rhxT6/yZeLOCja4cZnghJrNGA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", "dev": true }, "postcss-selector-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-4.0.0.tgz", - "integrity": "sha512-5h+MvEjnzu1qy6MabjuoPatsGAjjDV9B24e7Cktjl+ClNtjVjmvAXjOFQr1u7RlWULKNGYaYVE4s+DIIQ4bOGA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", "dev": true, "requires": { - "cssesc": "^1.0.1", + "cssesc": "^2.0.0", "indexes-of": "^1.0.1", "uniq": "^1.0.1" } diff --git a/package.json b/package.json index d211bc0b..7f699e70 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "browser-sync": "^2.26.3", "browser-sync-webpack-plugin": "^2.2.2", "copy-webpack-plugin": "^4.6.0", - "css-loader": "^2.0.2", + "css-loader": "^2.1.0", "custom-event-polyfill": "^1.0.6", "enzyme": "^3.8.0", "enzyme-adapter-react-16": "^1.7.1", @@ -104,11 +104,11 @@ "rollbar": "^2.5.1", "rollbar-sourcemap-webpack-plugin": "^2.5.0", "sass-loader": "^7.1.0", - "snapshot-diff": "^0.4.1", + "snapshot-diff": "^0.4.2", "style-loader": "^0.23.1", "stylelint": "^9.9.0", "stylelint-config-recommended-scss": "^3.2.0", - "stylelint-scss": "^3.4.3", + "stylelint-scss": "^3.4.4", "ts-jest": "^23.10.5", "ts-node": "^7.0.1", "tslint": "^5.12.0", diff --git a/src/js/editor.ts b/src/js/editor.ts index e8dfb465..908d9edb 100644 --- a/src/js/editor.ts +++ b/src/js/editor.ts @@ -4,6 +4,7 @@ import { registerPlugin } from '@wordpress/plugins'; import { registerFormatType } from '@wordpress/rich-text'; import bibliographyBlock from 'gutenberg/blocks/bibliography'; +import staticBibliographyBlock from 'gutenberg/blocks/static-bibliography'; import citationFormat from 'gutenberg/formats/citation'; import Sidebar from 'gutenberg/sidebar'; import { dataStore, uiStore } from 'stores'; @@ -19,3 +20,4 @@ registerPlugin('academic-bloggers-toolkit', { registerFormatType('abt/citation', citationFormat); registerBlockType('abt/bibliography', bibliographyBlock); +registerBlockType('abt/static-bibliography', staticBibliographyBlock); diff --git a/src/js/gutenberg/blocks/index.ts b/src/js/gutenberg/blocks/index.ts index 64bee88b..45cbbc42 100644 --- a/src/js/gutenberg/blocks/index.ts +++ b/src/js/gutenberg/blocks/index.ts @@ -2,3 +2,36 @@ export interface BibItem { content: string; id: string; } + +export function stripListItem(item: string): string; +export function stripListItem(item: Element): string; +export function stripListItem(item: Element | string): string { + if (typeof item === 'string') { + const container = document.createElement('div'); + container.innerHTML = item; + const child = container.querySelector('.csl-entry'); + if (child) { + item = child; + } else { + throw new Error( + 'Outer HTML of item must be a div with className "csl-entry"', + ); + } + } + const content = item; + let toRemove: Element[] = []; + for (const el of item.children) { + if (el.classList.contains('csl-indent')) { + break; + } + if (el.classList.contains('csl-left-margin')) { + toRemove = [...toRemove, el]; + continue; + } + if (el.classList.contains('csl-right-inline')) { + el.outerHTML = el.innerHTML; + } + } + toRemove.forEach(el => content.removeChild(el)); + return content.innerHTML.trim(); +} diff --git a/src/js/gutenberg/blocks/static-bibliography/edit.tsx b/src/js/gutenberg/blocks/static-bibliography/edit.tsx new file mode 100644 index 00000000..e736b55a --- /dev/null +++ b/src/js/gutenberg/blocks/static-bibliography/edit.tsx @@ -0,0 +1,258 @@ +import { BlockEditProps } from '@wordpress/blocks'; +import { + IconButton, + PanelBody, + Placeholder, + Toolbar, +} from '@wordpress/components'; +import { compose } from '@wordpress/compose'; +import { select as globalSelect, withSelect } from '@wordpress/data'; +import { + BlockFormatControls, + InspectorControls, + RichText, +} from '@wordpress/editor'; +import { Component, ComponentType } from '@wordpress/element'; +import classNames from 'classnames'; + +import CountIcon from 'gutenberg/components/count-icon'; +import SidebarItemList from 'gutenberg/components/sidebar-item-list'; +import { localeCache, styleCache } from 'utils/cache'; +import { swapWith } from 'utils/data'; +import Processor from 'utils/processor'; + +import { stripListItem } from '../'; +import { Attributes } from './'; +import styles from './style.scss'; + +namespace Edit { + export interface SelectProps { + readonly references: ReadonlyArray; + } + export type OwnProps = BlockEditProps; + export type Props = OwnProps & SelectProps; +} +class Edit extends Component { + render() { + const { + attributes: { items, orderedList }, + isSelected, + references, + setAttributes, + } = this.props; + const ListTag = orderedList ? 'ol' : 'ul'; + return ( + <> + + + Double click on one or more items in the list below to + insert them into the end of the list. + } + initialOpen={false} + title="Available References" + > + + + + + + + +
+ {items.length === 0 && ( + + Add references to this list by double clicking one + or more items listed in the "Avaliable References" + section of the block settings panel. + + )} + {items.length > 0 && ( + + {items.map(({ content, id }, i) => + isSelected ? ( + + setAttributes({ + items: [ + ...items.slice(0, i), + ...items.slice(i + 1), + ], + }) + } + onMoveDown={this.makeItemMover( + i, + 'down', + )} + /> + ) : ( + + key={id} + data-id={id} + tagName="li" + className={classNames( + 'csl-entry', + styles.item, + )} + value={content} + /> + ), + )} + + )} +
+ + ); + } + + private makeItemMover = (index: number, dir: 'up' | 'down') => { + const { + attributes: { items }, + setAttributes, + } = this.props; + if ( + (dir === 'up' && index === 0) || + (dir === 'down' && index === items.length - 1) + ) { + return; + } + return () => + setAttributes({ + items: swapWith(items, index, index + (dir === 'up' ? -1 : 1)), + }); + }; + + private addItem = async (id: string) => { + const { + attributes: { items }, + references, + setAttributes, + } = this.props; + const data = references.find(item => item.id === id); + if (!data) { + return; + } + const styleXml = await styleCache.fetchItem( + globalSelect('abt/data').getStyle().value, + ); + await localeCache.fetchItem(styleXml); + const processor = new Processor(styleXml); + processor.parseCitations([ + { + citationID: '', + citationItems: [ + { + id: data.id, + item: data, + }, + ], + properties: { + index: 0, + noteIndex: 0, + }, + }, + ]); + const newItem = processor.bibliography[0]; + if (newItem) { + setAttributes({ + items: [ + ...items, + { + ...newItem, + content: stripListItem(newItem.content), + }, + ], + }); + } + }; +} + +namespace LiveItem { + export interface Props { + id: string; + content: string; + onRemove(): void; + onMoveDown?(): void; + onMoveUp?(): void; + } +} +const LiveItem = ({ + content, + id, + onMoveDown, + onMoveUp, + onRemove, +}: LiveItem.Props) => ( +
+ + tagName="li" + data-id={id} + className={classNames('csl-entry', styles.item)} + value={content} + /> +
+ onMoveUp && onMoveUp()} + /> + onRemove()} /> + onMoveDown && onMoveDown()} + /> +
+
+); + +export default compose([ + withSelect( + (select, { attributes: { items } }) => { + const existingIds = items.map(({ id }) => id); + return { + references: select('abt/data') + .getSortedItems() + .filter(item => !existingIds.includes(item.id)), + }; + }, + ), +])(Edit) as ComponentType; diff --git a/src/js/gutenberg/blocks/static-bibliography/index.tsx b/src/js/gutenberg/blocks/static-bibliography/index.tsx new file mode 100644 index 00000000..39411420 --- /dev/null +++ b/src/js/gutenberg/blocks/static-bibliography/index.tsx @@ -0,0 +1,81 @@ +import { BlockConfig, createBlock } from '@wordpress/blocks'; +import uuid from 'uuid/v4'; + +import { BibItem, stripListItem } from '../'; +import edit from './edit'; +import save from './save'; + +export interface Attributes { + items: BibItem[]; + orderedList: boolean; +} + +const config: BlockConfig = { + title: 'Static Bibliography', + category: 'widgets', + description: 'Display a static list of references.', + icon: 'welcome-learn-more', + keywords: ['reference', 'citation', 'sources'], + attributes: { + items: { + type: 'array', + source: 'query', + selector: 'li', + default: [], + query: { + content: { + type: 'string', + source: 'html', + }, + id: { + type: 'string', + source: 'attribute', + attribute: 'data-id', + }, + }, + }, + orderedList: { + type: 'boolean', + default: true, + }, + }, + supports: { + anchor: true, + html: false, + }, + transforms: { + from: [ + { + type: 'raw', + selector: '.abt-static-bib', + transform(node: HTMLDivElement) { + const body = node.querySelector( + '.abt-bibliography__container', + ); + if (body) { + const items = [...body.children].map(item => { + const content = item.querySelector( + '.csl-entry', + ); + const id = item.id || uuid(); + return { + id, + content: content + ? stripListItem(content) + : item.innerHTML, + }; + }); + return createBlock('abt/static-bibliography', { + items, + }); + } + return; + }, + }, + ], + }, + edit, + save, +}; + +export default config; diff --git a/src/js/gutenberg/blocks/static-bibliography/save.tsx b/src/js/gutenberg/blocks/static-bibliography/save.tsx new file mode 100644 index 00000000..d12f8856 --- /dev/null +++ b/src/js/gutenberg/blocks/static-bibliography/save.tsx @@ -0,0 +1,29 @@ +import { BlockSaveProps } from '@wordpress/blocks'; +import { RichText } from '@wordpress/editor'; +import { Component } from '@wordpress/element'; + +import { Attributes } from './'; + +class BibliographySave extends Component> { + render() { + const { items, orderedList } = this.props.attributes; + const ListTag = orderedList ? 'ol' : 'ul'; + return ( +
+ + {items.map(({ content, id }, i) => ( + + key={i} + data-id={id} + tagName="li" + className="csl-entry" + value={content} + /> + ))} + +
+ ); + } +} + +export default BibliographySave; diff --git a/src/js/gutenberg/blocks/static-bibliography/style.scss b/src/js/gutenberg/blocks/static-bibliography/style.scss new file mode 100644 index 00000000..af0021d9 --- /dev/null +++ b/src/js/gutenberg/blocks/static-bibliography/style.scss @@ -0,0 +1,47 @@ +.body { + font-size: 0.8em; +} + +.item { + margin-left: 1em; + + > div:not([class]) { + display: inline; + display: contents; + } +} + +.button-list { + $icon-size: 15px; + $padding: 6px; + + display: grid; + opacity: 0; + + button { + padding: $padding; + overflow: visible; + + &:disabled { + cursor: not-allowed; + } + + svg { + width: $icon-size; + height: $icon-size; + } + } +} + +.row { + display: grid; + grid-template-columns: 100% min-content; + gap: 20px; + align-items: center; + + &:hover { + .button-list { + opacity: 1; + } + } +} diff --git a/src/js/utils/data.ts b/src/js/utils/data.ts index c9dec7de..91f817ba 100644 --- a/src/js/utils/data.ts +++ b/src/js/utils/data.ts @@ -7,6 +7,12 @@ export function clone(data: T): T { return JSON.parse(JSON.stringify(data)); } +export function swapWith(arr: T[], a: number, b: number): T[] { + const list = [...arr]; + const item = list.splice(a, 1); + return [...list.slice(0, b), ...item, ...list.slice(b)]; +} + export namespace date { export function getYear(d?: CSL.Date): number | string { if (!d) { diff --git a/types/wordpress__components/index.d.ts b/types/wordpress__components/index.d.ts index 9a1076a8..34727b95 100644 --- a/types/wordpress__components/index.d.ts +++ b/types/wordpress__components/index.d.ts @@ -437,6 +437,15 @@ export namespace PanelRow { } export const PanelRow: ComponentType; +export namespace Placeholder { + interface Props extends HTMLProps { + icon?: ReactNode; + label?: string; + instructions?: string; + } +} +export const Placeholder: ComponentType; + export namespace SelectControl { interface CommonProps { label?: string;