Add AccordionEditor (#2431)

This commit is contained in:
Alexander Platov 2022-12-12 07:39:03 +03:00 committed by GitHub
parent 8865006d3f
commit 130a19daa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 303 additions and 21 deletions

View File

@ -29,6 +29,12 @@
export function submit (): void {
textEditor.submit()
}
export function isEditable (): boolean {
return textEditor.isEditable()
}
export function setEditable (editable: boolean): void {
textEditor.setEditable(editable)
}
const dispatch = createEventDispatcher()
let focused = false

View File

@ -69,6 +69,18 @@
export function submit (): void {
textEditor.submit()
}
export function focus (): void {
textEditor.focus()
}
export function isEditable (): boolean {
return textEditor.isEditable()
}
export function setEditable (editable: boolean): void {
textEditor.setEditable(editable)
}
export function setContent (data: string): void {
textEditor.setContent(data)
}
const dispatch = createEventDispatcher()
let focused = false
@ -124,7 +136,7 @@
if (alwaysEdit) {
content = evt.detail
}
dispatch('changeContent')
dispatch('changeContent', evt.detail)
}}
>
{#if !alwaysEdit && !hideExtraButtons}

View File

@ -76,6 +76,18 @@
export function focus (): void {
textEditor.focus()
}
export function isEditable (): boolean {
return textEditor.isEditable()
}
export function setEditable (editable: boolean): void {
textEditor.setEditable(editable)
}
export function getContent (): string {
return content
}
export function setContent (data: string): void {
textEditor.setContent(data)
}
$: varsStyle =
maxHeight === 'card'
@ -344,7 +356,8 @@
}
</script>
<div class="ref-container clear-mins">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="ref-container clear-mins" tabindex="-1" on:click|preventDefault|stopPropagation={() => (needFocus = true)}>
{#if isFormatting}
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
<StyleButton
@ -551,8 +564,8 @@
.formatPanel {
position: sticky;
top: 1.25rem;
margin: -0.5rem -0.5rem 0.25rem;
padding: 0.5rem;
margin: -0.5rem -0.25rem 0.5rem;
padding: 0.375rem;
background-color: var(--body-accent);
border-radius: 0.5rem;
box-shadow: var(--button-shadow);

View File

@ -17,6 +17,7 @@
import { IntlString, translate } from '@hcengineering/platform'
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
import type { FocusPosition } from '@tiptap/core'
// import Typography from '@tiptap/extension-typography'
import Placeholder from '@tiptap/extension-placeholder'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
@ -42,6 +43,12 @@
const dispatch = createEventDispatcher()
export function isEditable (): boolean {
return editor.isEditable
}
export function setEditable (editable: boolean): void {
if (editor) editor.setEditable(editable)
}
export function submit (): void {
if (!editor.isEmpty) {
content = editor.getHTML()
@ -135,14 +142,17 @@
let needFocus = false
let focused = false
let posFocus: FocusPosition | undefined = undefined
export function focus (): void {
export function focus (position?: FocusPosition): void {
posFocus = position
needFocus = true
}
$: if (editor && needFocus) {
if (!focused) {
editor.commands.focus()
editor.commands.focus(posFocus)
posFocus = undefined
}
needFocus = false
}

View File

@ -360,3 +360,113 @@
}
}
// Accordion
.antiAccordion {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
.description {
// overflow: hidden;
padding: 0.75rem;
background-color: var(--body-accent);
border: 1px solid var(--button-border-color);
// border-radius: .5rem;
transition-property: background-color, height;
transition-duration: .15s;
transition-timing-function: var(--timing-main);
.label {
color: var(--dark-color);
text-shadow: var(--button-shadow);
}
.caption {
display: flex;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin: -.5rem;
padding: .5rem .5rem .5rem 1rem;
margin-bottom: 1rem;
min-width: 0;
font-weight: 500;
font-size: 1rem;
color: var(--caption-color);
background: var(--popup-bg-color);
border: 1px solid transparent;
border-radius: .125rem;
transition: margin-bottom .15s var(--timing-main),
border-radius .3s var(--timing-main),
box-shadow .15s var(--timing-main);
box-shadow: 0 0 .25rem .125rem #00000020;
// cursor: pointer;
z-index: 1;
.value {
overflow: auto;
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 0 .5rem;
max-height: 1.5rem;
font-weight: 400;
transition: opacity .15s var(--timing-main);
}
.rotated-icon {
transform-origin: center;
transition: transform .3s var(--timing-rotate);
&.opened { transform: rotate(0deg); }
&.closed { transform: rotate(90deg); }
}
}
&.opened {
.caption .value { opacity: 0; }
.expand-collapse {
visibility: visible;
max-height: max-content;
}
}
&.closed {
.caption {
margin-bottom: -.5rem;
.value { opacity: 1; }
}
.expand-collapse {
overflow: hidden;
visibility: hidden;
max-height: 0;
}
&:hover .caption { margin-bottom: 0rem; }
}
&:first-child {
border-top-left-radius: .75rem;
border-top-right-radius: .75rem;
}
&:first-child .caption {
border-top-left-radius: .65rem;
border-top-right-radius: .65rem;
}
&:last-child {
border-top: none;
border-bottom-left-radius: .75rem;
border-bottom-right-radius: .75rem;
}
&:last-child.closed .caption {
border-bottom-left-radius: .65rem;
border-bottom-right-radius: .65rem;
}
&:not(:first-child):not(:last-child) { border-top: none; }
&:hover,
&:focus-within { background-color: var(--body-color); }
// &:focus-within .caption { box-shadow: 0 0 2px 1px var(--primary-edit-border-color); }
&:focus-within .caption { border-color: var(--primary-edit-border-color); }
}
}

View File

@ -69,6 +69,7 @@
--font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
--timing-shadow: cubic-bezier(0,.65,.35,1);
--timing-main: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--timing-rotate: cubic-bezier(.28,1.92,.39,.56);
// transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
:root {

View File

@ -45,7 +45,7 @@
let iconSize: ButtonSize
$: iconSize = size === 'inline' ? 'inline' : 'small'
$: iconOnly = label === undefined && $$slots.content === undefined
$: iconOnly = label === undefined && ($$slots.content === undefined || $$slots.icon !== undefined)
onMount(() => {
if (focus && input) {
@ -114,9 +114,8 @@
<Label {label} params={labelParams} />
</span>
{/if}
{#if $$slots.content}
<slot name="content" />
{/if}
{#if $$slots.icon}<slot name="icon" />{/if}
{#if $$slots.content}<slot name="content" />{/if}
</button>
<style lang="scss">
@ -354,6 +353,9 @@
&:hover {
background-color: var(--primary-bg-hover);
}
&:focus {
border-color: var(--primary-edit-border-color);
}
&:disabled {
background-color: #5e6ad255;
border-color: #5e6ad255;

View File

@ -1,5 +1,5 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large' | 'full'
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'full'
const fill: string = 'currentColor'
</script>

View File

@ -0,0 +1,109 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Doc, Ref, Space, Class } from '@hcengineering/core'
import { Button, Label, IconDownOutline, tooltip } from '@hcengineering/ui'
import textEditorPlugin, { TextEditor } from '@hcengineering/text-editor'
import type { AccordionItem } from '..'
import AttachmentStyledBox from './AttachmentStyledBox.svelte'
export let items: AccordionItem[]
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
export function createAttachments (): void {
attachments.forEach((at) => at.createAttachments())
}
const dispatch = createEventDispatcher()
const attachments: AttachmentStyledBox[] = []
const edits: TextEditor[] = []
const flip = (index: number, ev?: MouseEvent): void => {
ev?.stopPropagation()
const cont = items[index].content
switch (items[index].state) {
case 'opened':
attachments[index].setEditable(false)
items[index].state = 'closed'
setTimeout(() => edits[index].focus('end'), 0)
break
case 'closed':
items[index].state = 'opened'
attachments[index].setEditable(true)
attachments[index].setContent(cont)
attachments[index].focus()
break
}
}
</script>
<div class="antiAccordion">
{#each items as item, i}
<div class="description {item.state}">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="caption"
use:tooltip={{ label: item.tooltip }}
tabindex="-1"
on:click={() => {
if (item.state === 'closed') edits[i].focus()
else attachments[i].focus()
}}
>
<span class="label"><Label label={item.label} /></span>
<div class="value">
{#if item.state === 'closed'}
<TextEditor
bind:content={item.content}
bind:this={edits[i]}
on:value={(ev) => {
dispatch('update', { item, value: ev.detail })
}}
on:content={(ev) => {
items[i].content = ev.detail
dispatch('update', { item, value: ev.detail })
flip(i)
}}
/>
{/if}
</div>
<Button size={'medium'} kind={'transparent'} on:click={(ev) => flip(i, ev)}>
<svelte:fragment slot="icon">
<div class="rotated-icon {item.state}">
<IconDownOutline size={'medium'} />
</div>
</svelte:fragment>
</Button>
</div>
<div class="expand-collapse">
<AttachmentStyledBox
bind:this={attachments[i]}
alwaysEdit
showButtons
bind:content={item.content}
placeholder={textEditorPlugin.string.EditorPlaceholder}
{objectId}
{_class}
{space}
on:changeContent={(ev) => dispatch('update', { item, value: ev.detail })}
/>
</div>
</div>
{/each}
</div>

View File

@ -37,6 +37,18 @@
export let focusable: boolean = false
export let refContainer: HTMLElement | undefined = undefined
export function focus (): void {
refInput.focus()
}
export function isEditable (): boolean {
return refInput.isEditable()
}
export function setEditable (editable: boolean): void {
refInput.setEditable(editable)
}
export function setContent (data: string): void {
refInput.setContent(data)
}
export function attach (): void {
inputFile.click()
}
@ -229,6 +241,7 @@
{focusable}
{emphasized}
on:changeSize
on:changeContent
on:attach={() => attach()}
/>
{#if attachments.size}
@ -250,6 +263,7 @@
<style lang="scss">
.list {
margin-top: 0.5rem;
padding: 0.5rem;
min-width: 0;
color: var(--theme-caption-color);

View File

@ -14,7 +14,7 @@
//
import attachment, { Attachment } from '@hcengineering/attachment'
import { ObjQueryType, SortingOrder, SortingQuery } from '@hcengineering/core'
import { ObjQueryType, SortingOrder, SortingQuery, Markup } from '@hcengineering/core'
import { IntlString, Resources } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient } from '@hcengineering/presentation'
@ -32,6 +32,7 @@ import FileBrowser from './components/FileBrowser.svelte'
import FileDownload from './components/icons/FileDownload.svelte'
import Photos from './components/Photos.svelte'
import AttachmentStyledBox from './components/AttachmentStyledBox.svelte'
import AccordionEditor from './components/AccordionEditor.svelte'
import { deleteFile, uploadFile } from './utils'
import { DisplayTx } from '@hcengineering/activity'
@ -47,7 +48,8 @@ export {
AttachmentDocList,
FileDownload,
FileBrowser,
AttachmentStyledBox
AttachmentStyledBox,
AccordionEditor
}
export enum FileBrowserSortMode {
@ -251,3 +253,12 @@ export default async (): Promise<Resources> => ({
DeleteAttachment
}
})
export interface AccordionItem {
id: string
label: IntlString
tooltip: IntlString
content: Markup
state: 'opened' | 'closed'
placeholder?: IntlString
}

View File

@ -67,7 +67,7 @@
})
}
function showCreateDialog (ev: MouseEvent) {
function showCreateDialog () {
if (createComponent === undefined) return
showPopup(createComponent, createComponentProps, 'top')
}
@ -91,13 +91,7 @@
</div>
<div class="ac-header-full" class:secondRow={twoRows}>
{#if createLabel && createComponent}
<Button
label={createLabel}
icon={IconAdd}
kind={'primary'}
size={'small'}
on:click={(ev) => showCreateDialog(ev)}
/>
<Button label={createLabel} icon={IconAdd} kind={'primary'} size={'small'} on:click={() => showCreateDialog()} />
{/if}
<ViewletSettingButton viewlet={descr} />
</div>