From 1848089afeb8602a876940a745409599b98c769e Mon Sep 17 00:00:00 2001 From: Yuki Hattori Date: Wed, 22 Apr 2020 01:50:25 +0900 Subject: [PATCH 1/5] Generate permalink to specific JSX in REPL --- demo/convert.js | 33 ++++++++++++++++++ demo/example.js | 10 ++++-- demo/index.js | 84 ++++++++++++++++------------------------------ demo/parse-hash.js | 67 ++++++++++++++++++++++++++++++++++++ package.json | 1 + yarn.lock | 2 +- 6 files changed, 137 insertions(+), 60 deletions(-) create mode 100644 demo/convert.js create mode 100644 demo/parse-hash.js diff --git a/demo/convert.js b/demo/convert.js new file mode 100644 index 00000000..c8789aad --- /dev/null +++ b/demo/convert.js @@ -0,0 +1,33 @@ +import { JSXSlack, jsxslack } from '../src/index' +import { isValidComponent } from '../src/jsx' + +const generateUrl = (params) => { + const q = new URLSearchParams() + Object.keys(params).forEach((k) => q.append(k, params[k])) + + return `https://api.slack.com/tools/block-kit-builder?${q}` +} + +export const convert = (jsx) => { + const output = jsxslack([jsx]) + + if (!JSXSlack.isValidElement(output)) + throw new Error('Cannot parse as jsx-slack component.') + + const ret = { text: JSON.stringify(output, null, 2) } + const encoded = JSON.stringify(output).replace(/\+/g, '%2b') + + if (isValidComponent(output.$$jsxslack.type)) { + const { name } = output.$$jsxslack.type.$$jsxslackComponent + + if (name === 'Blocks') { + ret.url = generateUrl({ blocks: encoded, mode: 'message' }) + } else if (name === 'Modal') { + ret.url = generateUrl({ view: encoded, mode: 'modal' }) + } else if (name === 'Home') { + ret.url = generateUrl({ view: encoded, mode: 'appHome' }) + } + } + + return ret +} diff --git a/demo/example.js b/demo/example.js index 2ea71801..d24ada86 100644 --- a/demo/example.js +++ b/demo/example.js @@ -1,4 +1,4 @@ -export const message = ` +const message = `

@@ -25,7 +25,7 @@ export const message = ` `.trim() -export const modal = ` +const modal = `

@@ -52,7 +52,7 @@ export const modal = ` `.trim() -export const home = ` +const home = ` home

@@ -87,3 +87,7 @@ export const home = ` `.trim() + +export default Object.freeze( + Object.assign(Object.create(null), { message, modal, home }) +) diff --git a/demo/index.js b/demo/index.js index 384bd400..f2b539ee 100644 --- a/demo/index.js +++ b/demo/index.js @@ -1,7 +1,9 @@ import CodeMirror from 'codemirror' import debounce from 'lodash.debounce' import { JSXSlack, jsxslack } from '../src/index' -import * as _examples from './example' +import { convert } from './convert' +import examples from './example' +import { parseHash, setJSXHash } from './parse-hash' import schema from './schema' import 'codemirror/mode/javascript/javascript' @@ -21,12 +23,13 @@ const errorDetails = document.getElementById('errorDetails') const examplesSelect = document.getElementById('examples') const previewBtn = document.getElementById('preview') -const parseHash = (hash = window.location.hash) => { - if (!hash.toString().startsWith('#')) return undefined - return decodeURIComponent(hash.toString().slice(1)) -} +// Parse hash +const initialValue = parseHash() -const examples = Object.assign(Object.create(null), _examples) +if (initialValue.example) { + examplesSelect.value = initialValue.example + if (examplesSelect.value !== initialValue.example) examplesSelect.value = '' +} // CodeMirror const completeAfter = (cm, pred) => { @@ -70,65 +73,29 @@ const jsxEditor = CodeMirror(jsx, { indentUnit: 2, lineWrapping: true, mode: 'jsx', - value: (() => { - const hash = parseHash() - - if (examples[hash]) { - examplesSelect.value = hash - if (examplesSelect.value !== hash) examplesSelect.value = '' - - return examples[hash] - } - - return examples.message - })(), + value: initialValue.text, }) -const setPreview = (query) => { - previewBtn.removeAttribute('data-mode') - - if (query === false) { - previewBtn.setAttribute('tabindex', -1) - previewBtn.classList.add('disabled') - } else if (typeof query === 'object') { - const q = new URLSearchParams() - Object.keys(query).forEach((k) => q.append(k, query[k])) - - if (query.mode) previewBtn.setAttribute('data-mode', query.mode) - +const setPreview = (url) => { + if (url) { previewBtn.removeAttribute('tabindex') - previewBtn.setAttribute( - 'href', - `https://api.slack.com/tools/block-kit-builder?${q}` - ) + previewBtn.setAttribute('href', url) previewBtn.classList.remove('disabled') + } else { + previewBtn.setAttribute('tabindex', -1) + previewBtn.classList.add('disabled') } } -const convert = () => { +const process = () => { try { - const output = jsxslack([jsxEditor.getValue()]) - - if (!JSXSlack.isValidElement(output)) - throw new Error('Cannot parse as jsx-slack component.') + const { text, url } = convert(jsxEditor.getValue()) - const encoded = JSON.stringify(output).replace(/\+/g, '%2b') - - json.value = JSON.stringify(output, null, ' ') - - if (Array.isArray(output)) { - setPreview({ blocks: encoded, mode: 'message' }) - } else if (output.type === 'modal') { - setPreview({ view: encoded, mode: 'modal' }) - } else if (output.type === 'home') { - setPreview({ view: encoded, mode: 'appHome' }) - } else { - setPreview(false) - } + json.value = text + setPreview(url) error.classList.add('hide') } catch (e) { - // eslint-disable-next-line no-console console.error(e) errorDetails.textContent = e.message.trim() @@ -136,10 +103,15 @@ const convert = () => { } } -const onChangeEditor = debounce(convert, 600) +const debouncedProcess = debounce(process, 600) +const onChangeEditor = () => { + setJSXHash(jsxEditor.getValue()) + debouncedProcess() +} + jsxEditor.on('change', onChangeEditor) -convert() +process() examplesSelect.addEventListener('change', () => { if (examplesSelect.value) { @@ -148,7 +120,7 @@ examplesSelect.addEventListener('change', () => { window.location.hash = `#${examplesSelect.value}` jsxEditor.setValue(examples[examplesSelect.value]) - convert() + process() } }) diff --git a/demo/parse-hash.js b/demo/parse-hash.js new file mode 100644 index 00000000..b55670cf --- /dev/null +++ b/demo/parse-hash.js @@ -0,0 +1,67 @@ +import pako from 'pako' +import { convert } from './convert' +import examples from './example' + +const getHashBody = (hash) => + hash.startsWith('#') ? decodeURIComponent(hash.slice(1)) : undefined + +const inflateJSX = (base64url) => + pako.inflate( + Uint8Array.from( + atob(base64url.replace(/_/g, '/').replace(/-/g, '+')), + (v) => v.charCodeAt(0) + ), + { to: 'string' } + ) + +export const parseHash = ({ + hash = window.location.hash, + strict = false, +} = {}) => { + const hashBody = getHashBody(hash) + + if (examples[hashBody]) return { example: hashBody, text: examples[hashBody] } + if (hashBody && hashBody.startsWith('jsx:')) { + try { + return { jsx: true, text: inflateJSX(hashBody.slice(4)) } + } catch (e) { + if (strict) { + throw e + } else { + console.warn(e) + console.warn('Invalid hash for JSX: Fallback to the default.') + } + } + } + if (strict) throw new Error('Invalid hash error') + + return { text: examples.message } +} + +export const setJSXHash = (jsx) => { + window.location.hash = `#jsx:${btoa(String.fromCharCode(...pako.deflate(jsx))) + .replace(/\//g, '_') + .replace(/\+/g, '-')}` +} + +// Parse "bkb:" prefix to redirect into Block Kit Builder immediately +const { hash } = window.location + +if (hash.startsWith('#bkb:')) { + const targetHash = `#${hash.slice(5)}` + + try { + const { text } = parseHash({ hash: targetHash, strict: true }) + const { url } = convert(text) + + if (!url) throw new Error('Corresponded URL cannot generate.') + + document.body.style.display = 'none' + window.location.replace(url) + } catch (e) { + console.warn(e) + console.warn('Failed to parse JSX: Fallback to the original hash.') + + window.location.hash = targetHash + } +} diff --git a/package.json b/package.json index 6107363b..41947fc2 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "jest-junit": "^10.0.0", "lodash.debounce": "^4.0.8", "npm-run-all": "^4.1.5", + "pako": "^1.0.11", "parcel": "^1.12.4", "prettier": "^2.0.4", "puppeteer": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index 918c6c69..e58f8bae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5783,7 +5783,7 @@ pako@^0.2.5: resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU= -pako@~1.0.5: +pako@^1.0.11, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== From 299b823ce1b8f2479d1f656d4b0ea68ab6effc46 Mon Sep 17 00:00:00 2001 From: Yuki Hattori Date: Wed, 22 Apr 2020 01:57:24 +0900 Subject: [PATCH 2/5] Don't stack history when updating hash --- demo/parse-hash.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/demo/parse-hash.js b/demo/parse-hash.js index b55670cf..b0fe0de5 100644 --- a/demo/parse-hash.js +++ b/demo/parse-hash.js @@ -39,9 +39,11 @@ export const parseHash = ({ } export const setJSXHash = (jsx) => { - window.location.hash = `#jsx:${btoa(String.fromCharCode(...pako.deflate(jsx))) - .replace(/\//g, '_') - .replace(/\+/g, '-')}` + window.location.replace( + `#jsx:${btoa(String.fromCharCode(...pako.deflate(jsx))) + .replace(/\//g, '_') + .replace(/\+/g, '-')}` + ) } // Parse "bkb:" prefix to redirect into Block Kit Builder immediately @@ -62,6 +64,6 @@ if (hash.startsWith('#bkb:')) { console.warn(e) console.warn('Failed to parse JSX: Fallback to the original hash.') - window.location.hash = targetHash + window.location.replace(targetHash) } } From a80c6c2766ae500c552afa22ec74d61ab5275b86 Mon Sep 17 00:00:00 2001 From: Yuki Hattori Date: Wed, 22 Apr 2020 11:35:59 +0900 Subject: [PATCH 3/5] Unified design of demo page in cross-browser --- demo/index.css | 75 ++++++++++++++++++++++++++++++++------------------ demo/index.js | 2 +- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/demo/index.css b/demo/index.css index 3d2bb716..497bbee1 100644 --- a/demo/index.css +++ b/demo/index.css @@ -11,10 +11,10 @@ --button-active-bg: #e8e8e8; --button-color: #333; --border: rgba(0, 0, 0, 0.2); + --focused-border: #69f; --textarea-bg: #fff; --textarea-readonly-bg: #f6f6f6; - --textarea-focused: #69f; --error-bg: #fec; --error-color: #c00; @@ -61,23 +61,34 @@ a:not(.button) { font-size: 15px; height: 30px; justify-content: center; + outline: 0; overflow: hidden; padding: 0 20px; - transition: background-color linear 0.15s, box-shadow linear 0.15s; + transition: background-color linear 0.15s, filter linear 0.15s; user-select: none; + will-change: transform; /* to fix drop-shadow transition in Safari */ } -.button:hover { +.button:hover, .button:focus { background: var(--button-hover-bg); - box-shadow: 0 1px 3px rgba(128, 128, 128, 0.25); + filter: drop-shadow(0 1px 3px rgba(128, 128, 128, 0.25)); } .button:hover:active { background: var(--button-active-bg); - box-shadow: 0 1px 6px rgba(128, 128, 128, 0.35); + filter: drop-shadow(0 1px 6px rgba(128, 128, 128, 0.35)); transition: none; } +.button::-moz-focus-inner { + border: 0; +} + +.button:focus { + border-color: var(--focused-border); + box-shadow: 0 0 0 1px var(--background), inset 0 0 0 1px var(--background); +} + .button.disabled { pointer-events: none; opacity: 0.4; @@ -120,16 +131,24 @@ select { font-size: 15px; height: 30px; line-height: 29px; + outline: 0; padding: 0 30px 0 10px; - /* transition: background-color linear 0.15s, box-shadow linear 0.15s; */ - transition: box-shadow linear 0.15s; + transition: background-color linear 0.15s, filter linear 0.15s; } -select:hover { - box-shadow: 0 1px 3px rgba(128, 128, 128, 0.25); +select:hover, select:focus { + filter: drop-shadow(0 1px 3px rgba(128, 128, 128, 0.25)); + --button-bg: var(--button-hover-bg); +} - /* Transition for background-color makes strange options in Firefox :( */ - /* --button-bg: var(--button-hover-bg); */ +select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 var(--button-color); +} + +select:focus { + border-color: var(--focused-border); + box-shadow: 0 0 0 1px var(--background), inset 0 0 0 1px var(--background); } /* Header */ @@ -198,7 +217,7 @@ main > img { font-size: 16px; padding: 20px; outline: 0; - transition: border-color linear 0.15s; + transition: filter linear 0.15s; } #jsx { @@ -212,10 +231,9 @@ main > img { border-color: var(--border); color: var(--secondary-color); resize: none; -} - -#json:focus { - --border: var(--textarea-focused); + scrollbar-color: var(--border) transparent; + scrollbar-width: thin; + will-change: transform; /* to fix drop-shadow transition in Safari */ } #jsx > .CodeMirror { @@ -230,10 +248,14 @@ main > img { right: -1px; top: -1px; transition: inherit; + will-change: transform; /* to fix drop-shadow transition in Safari */ } +#json:focus, #jsx > .CodeMirror.CodeMirror-focused { - --border: var(--textarea-focused); + --border: var(--focused-border); + box-shadow: 0 0 0 1px var(--background), inset 0 0 0 1px var(--background); + filter: drop-shadow(0 1px 3px rgba(128, 128, 128, 0.25)); } #jsx > .CodeMirror .CodeMirror-lines { @@ -241,25 +263,24 @@ main > img { } #jsx > .CodeMirror .CodeMirror-vscrollbar { - outline: 0; cursor: auto; + outline: 0; + scrollbar-color: var(--border) transparent; + scrollbar-width: thin; } -#jsx > .CodeMirror .CodeMirror-vscrollbar::-webkit-scrollbar { - width: 7px; - height: 7px; -} - +#jsx > .CodeMirror .CodeMirror-vscrollbar::-webkit-scrollbar, #json::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 10px; + height: 10px; } #jsx > .CodeMirror .CodeMirror-vscrollbar::-webkit-scrollbar-thumb, #json::-webkit-scrollbar-thumb { background-color: var(--border); - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; + background-clip: padding-box; + border: 2px solid transparent; + border-radius: 8px; } #error { diff --git a/demo/index.js b/demo/index.js index f2b539ee..e9427134 100644 --- a/demo/index.js +++ b/demo/index.js @@ -78,7 +78,7 @@ const jsxEditor = CodeMirror(jsx, { const setPreview = (url) => { if (url) { - previewBtn.removeAttribute('tabindex') + previewBtn.setAttribute('tabindex', 0) previewBtn.setAttribute('href', url) previewBtn.classList.remove('disabled') } else { From a4825ae9d63ea1f0800234e7a8d180d8f02eb14f Mon Sep 17 00:00:00 2001 From: Yuki Hattori Date: Wed, 22 Apr 2020 13:45:32 +0900 Subject: [PATCH 4/5] Apply Prettier formatting --- demo/index.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/demo/index.css b/demo/index.css index 497bbee1..2107a13d 100644 --- a/demo/index.css +++ b/demo/index.css @@ -69,7 +69,8 @@ a:not(.button) { will-change: transform; /* to fix drop-shadow transition in Safari */ } -.button:hover, .button:focus { +.button:hover, +.button:focus { background: var(--button-hover-bg); filter: drop-shadow(0 1px 3px rgba(128, 128, 128, 0.25)); } @@ -136,7 +137,8 @@ select { transition: background-color linear 0.15s, filter linear 0.15s; } -select:hover, select:focus { +select:hover, +select:focus { filter: drop-shadow(0 1px 3px rgba(128, 128, 128, 0.25)); --button-bg: var(--button-hover-bg); } From c99b2373e65ce6584467975f9760550b54fa747b Mon Sep 17 00:00:00 2001 From: Yuki Hattori Date: Wed, 22 Apr 2020 13:45:53 +0900 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fcfa42..c9101764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ jsx-slack v2 has improved JSX structure and built-in components to output the re - `value` prop as an alias into `initialXXX` prop in some interactive components - Added JSDoc to many public APIs and components - Support new JSX transpile via `automatic` runtime in Babel >= 7.9 _(experimental)_ ([#142](https://github.com/speee/jsx-slack/pull/142)) +- REPL demo now generates the permalink to specific JSX ([#149](https://github.com/speee/jsx-slack/pull/149)) ### Fixed