-
+
Template
@@ -21,7 +21,7 @@
-
+
Context
@@ -30,13 +30,11 @@
-
+
Output
-
-
{{__('playground.loading')}}
-
+
diff --git a/docs/themes/navy/source/css/_partial/index.styl b/docs/themes/navy/source/css/_partial/index.styl
index ff59dea74..9a1ab78e3 100644
--- a/docs/themes/navy/source/css/_partial/index.styl
+++ b/docs/themes/navy/source/css/_partial/index.styl
@@ -166,7 +166,7 @@
&:hover
color: var(--color-link-hover)
-#used-by-wrap, #sponsors-wrap
+#sponsors-wrap
background: var(--color-content-bg)
.inner
h3
@@ -181,7 +181,7 @@
#contributors-wrap
border: none
- overflow: hidden;
+ overflow: hidden
.contributors
tr
diff --git a/docs/themes/navy/source/css/_partial/playground.styl b/docs/themes/navy/source/css/_partial/playground.styl
index 38d386311..7c56b759a 100644
--- a/docs/themes/navy/source/css/_partial/playground.styl
+++ b/docs/themes/navy/source/css/_partial/playground.styl
@@ -90,21 +90,18 @@
grid-row: 1
grid-column: 1
min-height: 0
- .pane-indicator
- background: var(--color-link)
+ --pane-dot: var(--color-link)
.area-data
grid-row: 2
grid-column: 1
min-height: 0
- .pane-indicator
- background: var(--highlight-orange)
+ --pane-dot: var(--highlight-orange)
.area-output
grid-column: 2
grid-row: 1 / -1
min-height: 0
min-width: 0
- .pane-indicator
- background: var(--highlight-green)
+ --pane-dot: var(--highlight-green)
@media mq-mobile
grid-row: auto
grid-column: 1
@@ -145,6 +142,66 @@
height: 8px
border-radius: 50%
flex-shrink: 0
+ background: unquote('color-mix(in srgb, var(--pane-dot) 38%, var(--color-border))')
+ transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease
+
+ &[data-state="active"]
+ background: var(--pane-dot)
+ animation: playground-dot-typing 0.85s ease-in-out infinite
+
+ &[data-state="pending"]
+ background: var(--highlight-yellow)
+
+ &[data-state="ok"]
+ background: var(--highlight-green)
+ animation: playground-dot-ok 0.45s ease-out
+
+ &[data-state="error"]
+ background: var(--highlight-red)
+ animation: playground-dot-error 0.35s ease-out
+
+ .area-output .pane-indicator
+ &[data-state="pending"]
+ animation: playground-dot-pending 0.55s ease-in-out infinite
+
+ @keyframes playground-dot-typing
+ 0%, 100%
+ transform: scale(1)
+ box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--pane-dot) 0%, transparent)')
+ 50%
+ transform: scale(1.2)
+ box-shadow: 0 0 0 4px unquote('color-mix(in srgb, var(--pane-dot) 28%, transparent)')
+
+ @keyframes playground-dot-pending
+ 0%, 100%
+ transform: scale(1)
+ opacity: 0.75
+ 50%
+ transform: scale(1.12)
+ opacity: 1
+
+ @keyframes playground-dot-ok
+ 0%
+ transform: scale(0.85)
+ box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--highlight-green) 50%, transparent)')
+ 70%
+ transform: scale(1.15)
+ box-shadow: 0 0 0 5px unquote('color-mix(in srgb, var(--highlight-green) 0%, transparent)')
+ 100%
+ transform: scale(1)
+ box-shadow: none
+
+ @keyframes playground-dot-error
+ 0%, 100%
+ transform: translateX(0)
+ 20%
+ transform: translateX(-2px)
+ 40%
+ transform: translateX(2px)
+ 60%
+ transform: translateX(-1px)
+ 80%
+ transform: translateX(1px)
.pane-body
flex: 1 1 auto
@@ -156,27 +213,33 @@
background: var(--highlight-background)
.area-tpl .pane-body,
- .area-data .pane-body
+ .area-data .pane-body,
+ .area-output .pane-body
padding: var(--playground-inset)
box-sizing: border-box
@media mq-mobile
padding: 12px
.area-tpl .ace_gutter,
- .area-data .ace_gutter
- display: none !important
- width: 0 !important
- min-width: 0 !important
+ .area-data .ace_gutter,
+ .area-output .ace_gutter
+ display: none
+ width: 0
+ min-width: 0
.area-tpl .ace_editor,
.area-data .ace_editor,
+ .area-output .ace_editor,
.area-tpl .ace_scroller,
.area-data .ace_scroller,
+ .area-output .ace_scroller,
.area-tpl .ace_content,
.area-data .ace_content,
+ .area-output .ace_content,
.area-tpl .ace_text-layer,
- .area-data .ace_text-layer
- background: transparent !important
+ .area-data .ace_text-layer,
+ .area-output .ace_text-layer
+ background: transparent
.editor
flex: 1 1 auto
@@ -186,45 +249,9 @@
@media mq-mobile
min-height: 180px
- .output-preview
- flex: 1 1 auto
- min-height: 0
- min-width: 0
- width: 100%
- overflow: auto
+ .output-editor
@media mq-mobile
min-height: 120px
- pre.highlight
- margin: 0
- min-height: 100%
- width: 100%
- box-sizing: border-box
- padding: var(--playground-inset)
- border: none
- box-shadow: none
- border-radius: 0
- background: transparent
- color: var(--highlight-foreground)
- overflow-x: hidden
- overflow-y: auto
- white-space: pre-wrap
- overflow-wrap: break-word
- @media mq-mobile
- padding: 12px
- code
- display: block
- width: 100%
- box-sizing: border-box
- font-family: font-mono
- font-size: 14px
- line-height: 1.55
- color: var(--highlight-foreground)
- background: transparent
- padding: 0
- white-space: inherit
- overflow-wrap: inherit
- @media mq-mobile
- font-size: 13px
.ace_editor
font-family: font-mono
diff --git a/docs/themes/navy/source/js/main.js b/docs/themes/navy/source/js/main.js
index 278e90e49..abd4484d3 100644
--- a/docs/themes/navy/source/js/main.js
+++ b/docs/themes/navy/source/js/main.js
@@ -48,11 +48,15 @@
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
const editor = createEditor('editorEl', 'liquid');
const dataEditor = createEditor('dataEl', 'json');
- const previewCode = document.getElementById('previewCode');
+ const previewEditor = createEditor('previewEl', 'html', { readOnly: true });
+ const indicatorTpl = document.querySelector('.area-tpl .pane-indicator');
+ const indicatorData = document.querySelector('.area-data .pane-indicator');
+ const indicatorOutput = document.querySelector('.area-output .pane-indicator');
- const editors = [editor, dataEditor];
+ const editors = [editor, dataEditor, previewEditor];
let previewValue = '';
- let hadPreview = false;
+ let renderTimer = null;
+ const RENDER_DELAY = 180;
colorScheme.addEventListener('change', function() {
editors.forEach(applyEditorTheme);
if (previewValue) setPreview(previewValue);
@@ -63,9 +67,11 @@
editor.setValue(init.tpl, 1);
dataEditor.setValue(init.data, 1);
}
- editor.on('change', update);
- dataEditor.on('change', update);
- update();
+ editor.on('change', onTemplateChange);
+ dataEditor.on('change', onContextChange);
+ editor.on('focus', function () { setIndicator(indicatorTpl, 'active'); });
+ dataEditor.on('focus', function () { setIndicator(indicatorData, 'active'); });
+ scheduleUpdate();
ready();
function ready() {
@@ -91,7 +97,8 @@
editor.container.style.background = 'transparent';
}
- function createEditor(id, lang) {
+ function createEditor(id, lang, options) {
+ options = options || {};
const editor = ace.edit(id);
applyEditorTheme(editor);
editor.setOptions({
@@ -110,6 +117,10 @@
editor.renderer.$gutter.style.display = 'none';
}
editor.renderer.setScrollMargin(0, 0, 0, 0);
+ if (options.readOnly) {
+ editor.setReadOnly(true);
+ editor.getSession().setUseWrapMode(true);
+ }
bindClipboard(editor);
return editor;
}
@@ -166,23 +177,58 @@
return utoa(obj.tpl) + ',' + utoa(obj.data);
}
- function setPreview (value) {
- previewValue = value
- previewCode.textContent = value
+ function setPreview(value) {
+ previewValue = value;
+ previewEditor.setValue(value, -1);
+ previewEditor.clearSelection();
+ }
+
+ function setIndicator(indicator, state) {
+ if (indicator) indicator.dataset.state = state;
+ }
+
+ function onTemplateChange() {
+ setIndicator(indicatorTpl, 'active');
+ if (indicatorData.dataset.state !== 'error') setIndicator(indicatorData, 'idle');
+ setIndicator(indicatorOutput, 'pending');
+ scheduleUpdate();
+ }
+
+ function onContextChange() {
+ setIndicator(indicatorData, 'active');
+ if (indicatorTpl.dataset.state !== 'error') setIndicator(indicatorTpl, 'idle');
+ setIndicator(indicatorOutput, 'pending');
+ scheduleUpdate();
+ }
+
+ function scheduleUpdate() {
+ clearTimeout(renderTimer);
+ renderTimer = setTimeout(update, RENDER_DELAY);
}
async function update() {
const tpl = editor.getValue();
const data = dataEditor.getValue();
history.replaceState({}, '', '#' + serializeArgs({tpl, data}));
+ let parsed;
try {
- const html = await engine.parseAndRender(tpl, JSON.parse(data));
- if (html !== '' || !hadPreview) {
- setPreview(html);
- if (html !== '') hadPreview = true;
- }
+ parsed = JSON.parse(data);
} catch (err) {
- // keep last successful output while template or context is invalid
+ setIndicator(indicatorData, 'error');
+ setIndicator(indicatorTpl, 'idle');
+ setIndicator(indicatorOutput, 'error');
+ return;
+ }
+ try {
+ const html = await engine.parseAndRender(tpl, parsed);
+ setPreview(html);
+ setIndicator(indicatorTpl, 'idle');
+ setIndicator(indicatorData, 'idle');
+ setIndicator(indicatorOutput, 'ok');
+ } catch (err) {
+ setIndicator(indicatorTpl, 'error');
+ setIndicator(indicatorData, 'idle');
+ setIndicator(indicatorOutput, 'error');
}
}
diff --git a/package.json b/package.json
index 995de3f07..6bd02c13a 100644
--- a/package.json
+++ b/package.json
@@ -29,14 +29,15 @@
"build:min": "BUNDLES=min rollup -c rollup.config.mjs",
"build:umd": "BUNDLES=umd rollup -c rollup.config.mjs",
"build:charmap": "./bin/character-gen.js > src/util/character.ts",
- "build:docs": "run-s build:apidoc build:changelog build:docs-hexo",
+ "prepare:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog",
+ "build:docs": "run-s prepare:docs build:docs-hexo",
"build:docs-liquid": "cross-env BUNDLES=min rollup -c rollup.config.mjs && shx cp dist/liquid.browser.min.js docs/themes/navy/source/js/",
"build:contributors": "node bin/build-contributors.js",
"build:apidoc": "shx rm -rf docs/source/api && typedoc --plugin typedoc-plugin-missing-exports ./src --gitRevision master --out docs/source/api",
"build:changelog": "node bin/build-changelog.js",
"build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/",
- "docs:dev": "npm run docs:serve",
- "docs:serve": "cd docs && npm run start"
+ "serve:docs": "cd docs && npm run start",
+ "dev:docs": "run-s prepare:docs serve:docs"
},
"bin": {
"liquidjs": "./bin/liquid.js",