diff --git a/web/app/@types/markdown-toolbar-element/index.d.ts b/web/app/@types/markdown-toolbar-element/index.d.ts new file mode 100644 index 00000000..a8812c83 --- /dev/null +++ b/web/app/@types/markdown-toolbar-element/index.d.ts @@ -0,0 +1,17 @@ +declare namespace JSX { + interface IntrinsicElements { + 'markdown-toolbar': { + for: string; + children?: any; + className: string; + }; + 'md-bold': any; + 'md-header': any; + 'md-italic': any; + 'md-quote': any; + 'md-code': any; + 'md-link': any; + 'md-unordered-list': any; + 'md-ordered-list': any; + } +} diff --git a/web/app/common/closest-polyfill.js b/web/app/common/closest-polyfill.js new file mode 100644 index 00000000..d83819b2 --- /dev/null +++ b/web/app/common/closest-polyfill.js @@ -0,0 +1,15 @@ +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} + +if (!Element.prototype.closest) { + Element.prototype.closest = function(s) { + let el = this; + + do { + if (el.matches(s)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; +} diff --git a/web/app/common/polyfills.ts b/web/app/common/polyfills.ts index 970955f5..84e4c1c9 100644 --- a/web/app/common/polyfills.ts +++ b/web/app/common/polyfills.ts @@ -1,5 +1,7 @@ import 'core-js/es/promise'; import 'focus-visible'; +import '@webcomponents/custom-elements'; +import './closest-polyfill'; export default async function loadPolyfills() { const fillCoreJs = async () => { diff --git a/web/app/components/input/__control-panel/input__control-panel.scss b/web/app/components/input/__control-panel/input__control-panel.scss new file mode 100644 index 00000000..dbe5e17b --- /dev/null +++ b/web/app/components/input/__control-panel/input__control-panel.scss @@ -0,0 +1,4 @@ +.input__control-panel { + height: 30px; + background-color: #eee; +} diff --git a/web/app/components/input/__field/input__field.scss b/web/app/components/input/__field/input__field.scss index 8f50cb41..6531b563 100644 --- a/web/app/components/input/__field/input__field.scss +++ b/web/app/components/input/__field/input__field.scss @@ -2,7 +2,7 @@ $lineHeight: 1.4; $fontSize: 16px; $paddingVrt: 10px; - $lines: 3; + $lines: 4; $height: calc($fontSize * $lineHeight * $lines + $paddingVrt * 2); box-sizing: border-box; diff --git a/web/app/components/input/__markdown-toolbar/input__markdown-toolbar.scss b/web/app/components/input/__markdown-toolbar/input__markdown-toolbar.scss new file mode 100644 index 00000000..1122ca4e --- /dev/null +++ b/web/app/components/input/__markdown-toolbar/input__markdown-toolbar.scss @@ -0,0 +1,31 @@ +.input__toolbar { + display: block; + float: right; +} + +.input__toolbar-item { + background: none; + border: 0; + color: #586069; + display: block; + float: left; + padding: 4px 5px; + + &:hover { + color: #0aa; + } +} + +.input__toolbar-icon { + display: inline-block; + fill: currentColor; +} + +.input__toolbar-group { + display: inline-block; + margin-left: 20px; +} + +.input__toolbar-group:first-child { + margin-left: 0; +} diff --git a/web/app/components/input/_theme/_dark/input_theme_dark.scss b/web/app/components/input/_theme/_dark/input_theme_dark.scss index 252cb424..11d52051 100644 --- a/web/app/components/input/_theme/_dark/input_theme_dark.scss +++ b/web/app/components/input/_theme/_dark/input_theme_dark.scss @@ -35,4 +35,15 @@ .input__preview-wrapper { background: #eee; } + + .input__toolbar-item { + color: #ddd; + &:hover { + color: #0aa; + } + } + + .input__control-panel { + background-color: #333; + } } diff --git a/web/app/components/input/input.scss b/web/app/components/input/input.scss index 062b7960..8d43f7a6 100644 --- a/web/app/components/input/input.scss +++ b/web/app/components/input/input.scss @@ -2,6 +2,7 @@ position: relative; display: block; font-size: 0; - border: 12px solid; + border-style: solid; + border-width: 6px 12px 12px 12px; border-radius: 2px; } diff --git a/web/app/components/input/input.tsx b/web/app/components/input/input.tsx index daab9afd..d820b7a8 100644 --- a/web/app/components/input/input.tsx +++ b/web/app/components/input/input.tsx @@ -13,12 +13,15 @@ import { StaticStore } from '@app/common/static_store'; import { siteId, url, pageTitle } from '@app/common/settings'; import { extractErrorMessageFromResponse } from '@app/utils/errorUtils'; +import MarkdownToolbar from './markdown-toolbar'; import TextareaAutosize from './textarea-autosize'; const RSS_THREAD_URL = `${BASE_URL}${API_BASE}/rss/post?site=${siteId}&url=${url}`; const RSS_SITE_URL = `${BASE_URL}${API_BASE}/rss/site?site=${siteId}`; const RSS_REPLIES_URL = `${BASE_URL}${API_BASE}/rss/reply?site=${siteId}&user=`; +let textareaId = 0; + interface Props { /** user id for rss link generation */ userId?: User['id']; @@ -52,10 +55,11 @@ const Labels = { export class Input extends Component { textAreaRef?: TextareaAutosize; - + textareaId: string; constructor(props: Props) { super(props); - + textareaId = textareaId + 1; + this.textareaId = `textarea_${textareaId}`; this.state = { preview: null, isErrorShown: false, @@ -165,8 +169,12 @@ export class Input extends Component { onSubmit={this.send} aria-label="New comment" > +
+ +
(this.textAreaRef = ref)} className="input__field" placeholder="Your comment here" diff --git a/web/app/components/input/markdown-toolbar-icons/bold-icon.tsx b/web/app/components/input/markdown-toolbar-icons/bold-icon.tsx new file mode 100644 index 00000000..67575103 --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/bold-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function BoldIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/code-icon.tsx b/web/app/components/input/markdown-toolbar-icons/code-icon.tsx new file mode 100644 index 00000000..d2312313 --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/code-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function CodeIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/header-icon.tsx b/web/app/components/input/markdown-toolbar-icons/header-icon.tsx new file mode 100644 index 00000000..73be26a6 --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/header-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function HeaderIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/italic-icon.tsx b/web/app/components/input/markdown-toolbar-icons/italic-icon.tsx new file mode 100644 index 00000000..19fec985 --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/italic-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function ItalicIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/link-icon.tsx b/web/app/components/input/markdown-toolbar-icons/link-icon.tsx new file mode 100644 index 00000000..40b8cb2a --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/link-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function LinkIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/ordered-list-icon.tsx b/web/app/components/input/markdown-toolbar-icons/ordered-list-icon.tsx new file mode 100644 index 00000000..c58b1778 --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/ordered-list-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function OrderedListIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/quote-icon.tsx b/web/app/components/input/markdown-toolbar-icons/quote-icon.tsx new file mode 100644 index 00000000..aecdc2cf --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/quote-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function QuoteIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar-icons/unordered-list-icon.tsx b/web/app/components/input/markdown-toolbar-icons/unordered-list-icon.tsx new file mode 100644 index 00000000..a82ada9e --- /dev/null +++ b/web/app/components/input/markdown-toolbar-icons/unordered-list-icon.tsx @@ -0,0 +1,13 @@ +/** @jsx h */ +import { h } from 'preact'; + +export default function UnorderedListIcon() { + return ( + + ); +} diff --git a/web/app/components/input/markdown-toolbar.tsx b/web/app/components/input/markdown-toolbar.tsx new file mode 100644 index 00000000..a50c9ba5 --- /dev/null +++ b/web/app/components/input/markdown-toolbar.tsx @@ -0,0 +1,63 @@ +/** @jsx h */ +import { h, Component, RenderableProps } from 'preact'; +import '@github/markdown-toolbar-element'; +import BoldIcon from './markdown-toolbar-icons/bold-icon'; +import HeaderIcon from './markdown-toolbar-icons/header-icon'; +import ItalicIcon from './markdown-toolbar-icons/italic-icon'; +import QuoteIcon from './markdown-toolbar-icons/quote-icon'; +import CodeIcon from './markdown-toolbar-icons/code-icon'; +import LinkIcon from './markdown-toolbar-icons/link-icon'; +import UnorderedListIcon from './markdown-toolbar-icons/unordered-list-icon'; +import OrderedListIcon from './markdown-toolbar-icons/ordered-list-icon'; + +interface Props { + textareaId: string; +} + +const boldLabel = 'Add bold text '; +const headerLabel = 'Add header text'; +const italicLabel = 'Add italic text '; +const quoteLabel = 'Insert a quote'; +const codeLabel = 'Insert a code'; +const linkLabel = 'Add a link '; +const unorderedListLabel = 'Add a bulleted list'; +const orderedListLabel = 'Add a numbered list'; + +export default class MarkdownToolbar extends Component { + render(props: RenderableProps) { + return ( + +
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + +
+
+ ); + } +} diff --git a/web/app/components/input/styles.ts b/web/app/components/input/styles.ts index 077e3817..450bcf82 100644 --- a/web/app/components/input/styles.ts +++ b/web/app/components/input/styles.ts @@ -8,6 +8,7 @@ require('./__button/input__button.scss'); require('./__button/_type/_preview/input__button_type_preview.scss'); require('./__button/_type/_send/input__button_type_send.scss'); +require('./__control-panel/input__control-panel.scss'); require('./__counter/input__counter.scss'); require('./__error/input__error.scss'); require('./__field/input__field.scss'); @@ -18,6 +19,7 @@ require('./__rss/input__rss.scss'); require('./__rss-link/input__rss-link.scss'); require('./__markdown/input__markdown.scss'); require('./__markdown-link/input__markdown-link.scss'); +require('./__markdown-toolbar/input__markdown-toolbar.scss'); require('./_theme/_dark/input_theme_dark.scss'); require('./_theme/_light/input_theme_light.scss'); diff --git a/web/jest.config.js b/web/jest.config.js index 738bf9c7..07cc975f 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -11,4 +11,5 @@ module.exports = { '\\.scss$': '/app/testUtils/mockStyles.js', '@app/(.*)': '/app/$1', }, + setupFilesAfterEnv: ['/setup-jest-env.js'], }; diff --git a/web/package-lock.json b/web/package-lock.json index 80bdf9bb..d6838b69 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1188,6 +1188,11 @@ "minimist": "^1.2.0" } }, + "@github/markdown-toolbar-element": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-0.2.0.tgz", + "integrity": "sha512-PMb2OiJKmrte/JbOUnhqAh1fTUBdIi0DskpTmSo+Jg/HaBahjtKUlhxe0+sH33Dxxvg9OQLeFXuB6XL3F9Gtuw==" + }, "@jest/console": { "version": "24.7.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", @@ -1717,6 +1722,11 @@ "@xtuc/long": "4.2.2" } }, + "@webcomponents/custom-elements": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.2.2.tgz", + "integrity": "sha512-9lckVAFpDEma94Ass/DHXY5ZoLZI5p+ambJQnra40npWIq+SQynvUHsJjLVmYartb4lZH9u+HLgfiAEnQih10Q==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -3741,6 +3751,15 @@ "esutils": "^2.0.2" } }, + "document-register-element": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/document-register-element/-/document-register-element-1.13.1.tgz", + "integrity": "sha512-92ZyLDKg9j4rOll//NNXj25f+8rAzOkYsGJonhugKwXfeqH7bzs8Ucpvey0WzZ2ZzKdrvW9RnUw3UyOZ/uhBFw==", + "dev": true, + "requires": { + "lightercollective": "^0.1.0" + } + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -4835,8 +4854,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4857,14 +4875,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4879,20 +4895,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5009,8 +5022,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5022,7 +5034,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5037,7 +5048,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5045,14 +5055,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5071,7 +5079,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5152,8 +5159,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5165,7 +5171,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5251,8 +5256,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5288,7 +5292,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5308,7 +5311,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5352,14 +5354,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -7183,6 +7183,12 @@ "type-check": "~0.3.2" } }, + "lightercollective": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lightercollective/-/lightercollective-0.1.0.tgz", + "integrity": "sha512-J9tg5uraYoQKaWbmrzDDexbG6hHnMcWS1qLYgJSWE+mpA3U5OCSeMUhb+K55otgZJ34oFdR0ECvdIb3xuO5JOQ==", + "dev": true + }, "lint-staged": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-8.1.5.tgz", diff --git a/web/package.json b/web/package.json index 04f9ca4e..51ebfdcd 100644 --- a/web/package.json +++ b/web/package.json @@ -47,6 +47,7 @@ "clean-webpack-plugin": "^2.0.1", "copy-webpack-plugin": "^5.0.2", "css-loader": "^2.1.1", + "document-register-element": "^1.13.1", "eslint": "^5.16.0", "eslint-config-prettier": "^4.1.0", "eslint-plugin-jsx-a11y": "^6.2.1", @@ -80,6 +81,8 @@ "whatwg-fetch": "^3.0.0" }, "dependencies": { + "@github/markdown-toolbar-element": "^0.2.0", + "@webcomponents/custom-elements": "^1.2.2", "bem-react-helper": "^1.1.2", "core-js": "^3.0.1", "focus-visible": "^4.1.5", diff --git a/web/setup-jest-env.js b/web/setup-jest-env.js new file mode 100644 index 00000000..be67c9a2 --- /dev/null +++ b/web/setup-jest-env.js @@ -0,0 +1 @@ +require('document-register-element/pony')(window); diff --git a/web/webpack.config.js b/web/webpack.config.js index 47aef48c..7a735adf 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -62,8 +62,32 @@ module.exports = () => ({ rules: [ { test: /\.js(x?)$/, - exclude: /node_modules/, - use: 'babel-loader', + oneOf: [ + { + include: /node_modules\/@github\/markdown-toolbar-element/, + use: { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: { + browsers: ['> 1%', 'android >= 4.4.4', 'ios >= 9', 'IE >= 11'], + }, + useBuiltIns: 'usage', + corejs: 3, + }, + ], + ], + }, + }, + }, + { + exclude: /node_modules/, + use: 'babel-loader', + }, + ], }, { test: /\.ts(x?)$/,