diff --git a/pages/src/common/code.dark.less b/pages/src/common/code.dark.less new file mode 100644 index 00000000..fde80a77 --- /dev/null +++ b/pages/src/common/code.dark.less @@ -0,0 +1,82 @@ +.uip-root.dark-theme .uip-editor { + .editor-content { + color: #ccc; + text-shadow: none; + background-color: @dark-theme; + } + + .token.block-comment, + .token.cdata, + .token.comment, + .token.doctype, + .token.prolog { + color: #999; + } + + .token.punctuation { + color: #ccc; + } + + .token.attr-name, + .token.deleted, + .token.namespace, + .token.tag { + color: #e2777a; + } + + .token.function-name { + color: #6196cc; + } + + .token.boolean, + .token.function, + .token.number { + color: #f08d49; + } + + .token.class-name, + .token.constant, + .token.property, + .token.symbol { + color: #f8c555; + } + + .token.atrule, + .token.builtin, + .token.important, + .token.keyword, + .token.selector { + color: #cc99cd; + } + + .token.attr-value, + .token.char, + .token.regex, + .token.string, + .token.variable { + color: #7ec699; + } + + .token.entity, + .token.operator, + .token.url { + color: #67cdcc; + } + + .token.bold, + .token.important { + font-weight: 700; + } + + .token.italic { + font-style: italic; + } + + .token.entity { + cursor: help; + } + + .token.inserted { + color: green; + } +} diff --git a/pages/src/common/code.less b/pages/src/common/code.less index 00fe0733..e2c64596 100644 --- a/pages/src/common/code.less +++ b/pages/src/common/code.less @@ -4,3 +4,5 @@ pre { margin-bottom: 1rem; overflow: auto; } + +@import './code.dark.less'; diff --git a/pages/views/examples/example/form.njk b/pages/views/examples/example/form.njk index 10c2051a..9720c9d7 100644 --- a/pages/views/examples/example/form.njk +++ b/pages/views/examples/example/form.njk @@ -28,5 +28,5 @@ title: Example with form - + diff --git a/src/core/base/plugin.tsx b/src/core/base/plugin.tsx index c8d5f9e5..0f003eeb 100644 --- a/src/core/base/plugin.tsx +++ b/src/core/base/plugin.tsx @@ -30,9 +30,9 @@ export abstract class UIPPlugin extends ESLBaseElement { /** {@link UIPPlugin} section wrapper */ @memoize() - protected get $inner(): HTMLDivElement { + protected get $inner(): HTMLElement { const pluginType = this.constructor as typeof UIPPlugin; - return
as HTMLDivElement; + return
as HTMLElement; } protected connectedCallback(): void { diff --git a/src/core/base/root.less b/src/core/base/root.less index 484f679c..1209c1a0 100644 --- a/src/core/base/root.less +++ b/src/core/base/root.less @@ -4,6 +4,7 @@ .uip-root { position: relative; + overflow: hidden; display: grid; align-content: start; grid-template-columns: minmax(calc(100% - 250px), 100%) auto; @@ -25,15 +26,6 @@ 'editor'; } - &[editor-collapsed] > .uip-editor { - max-height: 0; - - &-inner { - visibility: hidden; - transition: visibility 0s 0.3s; - } - } - &[settings-collapsed] > .uip-settings { max-width: 0; max-height: 0; diff --git a/src/plugins/editor/editor.header.less b/src/plugins/editor/editor.header.less new file mode 100644 index 00000000..fdcaf04e --- /dev/null +++ b/src/plugins/editor/editor.header.less @@ -0,0 +1,93 @@ +html { + --uip-editor-divider: #ccc; + --uip-editor-header-bg: #ccc; + --uip-editor-header-fg: #000; +} + +.uip-editor-header { + position: relative; + display: flex; + align-items: center; + + color: var(--uip-editor-header-fg, #000); + background: var(--uip-editor-header-bg, transparent); + border-bottom: 1px solid var(--uip-editor-divider, transparent); + + &-title { + margin-inline-end: auto; + } + + &-title:not(:empty) { + padding: 5px; + } + + &-copy { + position: relative; + width: 25px; + height: 25px; + z-index: 2; + } + + &-trigger { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + + background: transparent; + border: none; + box-shadow: none; + appearance: none; + } + + &.no-label { + background: transparent; + justify-content: center; + } + + &.no-label &-title { + display: none; + } + + &.no-label &-trigger { + left: 50%; + width: 100px; + transform: translateX(-50%); + + background: var(--uip-editor-header-bg, transparent); + border-radius: 50px 50px 0 0; + opacity: 0.5; + transition: opacity 0.3s ease-in-out; + + &:hover { + opacity: 1; + } + } + + [collapsible] &::after { + content: ''; + position: relative; + margin: 5px 10px; + top: -2px; + width: 7px; + height: 7px; + z-index: 1; + pointer-events: none; + + border-left: 2px solid currentColor; + border-top: 2px solid currentColor; + + transform: rotate(225deg); + transform-origin: center; + transition: + transform 0.5s ease-in-out, + top 0.5s ease-in-out; + } + + [collapsible][collapsed] &::after { + top: 2px; + transform: rotate(45deg); + } +} diff --git a/src/plugins/editor/editor.less b/src/plugins/editor/editor.less index 117ed11e..813ad240 100644 --- a/src/plugins/editor/editor.less +++ b/src/plugins/editor/editor.less @@ -1,108 +1,64 @@ @import (reference) '../../common/variables.less'; +html { + --uip-editor-bg: #f5f2f0; +} + .uip-editor { - position: relative; - grid-area: editor; display: block; - max-height: 325px; - overflow: auto; - transition: max-height 0.3s ease-in-out; + grid-area: editor; &-inner { - height: 100%; - border-top: 1px solid @section-border; - padding: 0; + display: flex; + position: relative; + height: auto; + overflow: hidden; + max-height: 325px; + transition: + padding-top 0.3s linear, + padding-bottom 0.3s linear, + max-height 0.3s linear, + visibility 0s linear; + + padding: 1em; + background: var(--uip-editor-bg); } - .editor-content { - margin: 0; + &[collapsed] &-inner { + padding-top: 0; + padding-bottom: 0; + max-height: 0; + visibility: hidden; + transition: + padding-top 0.3s linear, + padding-bottom 0.3s linear, + max-height 0.3s linear, + visibility 0.3s linear; } - .codejar-linenumbers { - // Override default width set by withLineNumbers - width: unset !important; + &-container { + flex: 1 1 auto; + } + &-container &-code { + margin: 0; + padding: 0; } - .uip-root.dark-theme & { - .editor-content { - color: #ccc; - text-shadow: none; - background-color: @dark-theme; - } - - .token.block-comment, - .token.cdata, - .token.comment, - .token.doctype, - .token.prolog { - color: #999; - } - - .token.punctuation { - color: #ccc; - } - - .token.attr-name, - .token.deleted, - .token.namespace, - .token.tag { - color: #e2777a; - } - - .token.function-name { - color: #6196cc; - } - - .token.boolean, - .token.function, - .token.number { - color: #f08d49; - } - - .token.class-name, - .token.constant, - .token.property, - .token.symbol { - color: #f8c555; - } - - .token.atrule, - .token.builtin, - .token.important, - .token.keyword, - .token.selector { - color: #cc99cd; - } - - .token.attr-value, - .token.char, - .token.regex, - .token.string, - .token.variable { - color: #7ec699; - } - - .token.entity, - .token.operator, - .token.url { - color: #67cdcc; - } - - .token.bold, - .token.important { - font-weight: 700; - } - - .token.italic { - font-style: italic; - } + &-scrollbar { + flex: 0 0 auto; + order: -1; + opacity: 1; + transition: opacity 0.3s ease-in-out; + } - .token.entity { - cursor: help; - } + &[collapsed] &-scrollbar { + opacity: 0; + } - .token.inserted { - color: green; - } + .codejar-linenumbers { + // Override default width set by withLineNumbers + width: unset !important; } } + +@import "./editor.header.less"; diff --git a/src/plugins/editor/editor.tsx b/src/plugins/editor/editor.tsx index 71194ec7..99d7e30c 100644 --- a/src/plugins/editor/editor.tsx +++ b/src/plugins/editor/editor.tsx @@ -8,30 +8,69 @@ import Prism from 'prismjs'; import {CodeJar} from 'codejar'; import {debounce} from '@exadel/esl/modules/esl-utils/async/debounce'; -import {bind, decorate, memoize} from '@exadel/esl/modules/esl-utils/decorators'; +import {bind, boolAttr, decorate, listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; import {UIPPlugin} from '../../core/base/plugin'; +import {UIPOptionIcons} from '../header/options/option-icons'; /** * Editor {@link UIPPlugin} custom element definition * Uses Codejar code editor to provide an ability to modify UIP state markup - * @extends UIPPlugin */ export class UIPEditor extends UIPPlugin { public static override is = 'uip-editor'; + public static override observedAttributes = ['collapsible', 'copy', 'label']; /** Highlight method declaration */ public static highlight = (editor: HTMLElement): void => Prism.highlightElement(editor, false); - /** Wrapped {@link https://medv.io/codejar/ CodeJar} editor instance */ + /** Marker to collapse editor area */ + @boolAttr() public collapsed: boolean; + + /** Marker to make enable toggle collapse action for section header */ + @boolAttr() public collapsible: boolean; + + /** Marker to display copy widget */ + @boolAttr({name: 'copy'}) public showCopy: boolean; + + /** Header section block */ @memoize() - protected get editor(): CodeJar { - return CodeJar(this.$code, UIPEditor.highlight, { tab: '\t' }); + protected get $header(): HTMLElement { + const type = this.constructor as typeof UIPEditor; + return ( +
+ {this.label} + {this.showCopy ? {UIPOptionIcons.copySVG} : ''} + {this.collapsible ?
+ ) as HTMLElement; } + /** {@link UIPEditor} section wrapper */ + @memoize() + protected get $inner(): HTMLDivElement { + const type = this.constructor as typeof UIPPlugin; + return ( +
+ +
+ {this.$code} +
+
+ ) as HTMLDivElement; + } + + /** Code block */ @memoize() protected get $code(): HTMLElement { - return (
) as HTMLElement; + const type = this.constructor as typeof UIPEditor; + return (
) as HTMLElement; + } + + /** Wrapped {@link https://medv.io/codejar/ CodeJar} editor instance */ + @memoize() + protected get editor(): CodeJar { + return CodeJar(this.$code, UIPEditor.highlight, {tab: '\t'}); } /** @returns editor's content */ @@ -49,10 +88,8 @@ export class UIPEditor extends UIPPlugin { this.innerHTML = ''; // Prefill content + this.appendChild(this.$header); this.appendChild(this.$inner); - this.$inner.classList.add('esl-scrollable-content'); - this.$inner.append(); - this.$inner.append(this.$code); // Initial update this._onRootStateChange(); @@ -66,6 +103,20 @@ export class UIPEditor extends UIPPlugin { super.disconnectedCallback(); } + protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { + this.$header.remove(); + memoize.clear(this, '$header'); + this.insertAdjacentElement('afterbegin', this.$header); + } + + @listen({ + event: 'click', + selector: `.${UIPEditor.is}-header-trigger`, + }) + protected _onClick(): void { + if (this.collapsible) this.collapsed = !this.collapsed; + } + /** Callback to call on editor's content changes */ @decorate(debounce, 1000) protected _onChange(): void {