mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
Add AccordionEditor (#2431)
This commit is contained in:
parent
8865006d3f
commit
130a19daa9
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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); }
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user