UBER-593: hyperlink editor (#3506)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2023-07-19 17:46:03 +05:00 committed by GitHub
parent ac4eeb65f7
commit 3fcd9081be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 223 additions and 6 deletions

View File

@ -452,8 +452,8 @@ export function createModel (builder: Builder): void {
builder,
core.class.TypeHyperlink,
view.component.HyperlinkPresenter,
view.component.StringEditor,
view.component.StringEditorPopup
view.component.HyperlinkEditor,
view.component.HyperlinkEditorPopup
)
classPresenter(builder, core.class.TypeIntlString, view.component.IntlStringPresenter)
classPresenter(builder, core.class.TypeNumber, view.component.NumberPresenter, view.component.NumberEditor)

View File

@ -47,6 +47,8 @@ export default mergeIds(viewId, view, {
StringEditorPopup: '' as AnyComponent,
StringPresenter: '' as AnyComponent,
HyperlinkPresenter: '' as AnyComponent,
HyperlinkEditor: '' as AnyComponent,
HyperlinkEditorPopup: '' as AnyComponent,
IntlStringPresenter: '' as AnyComponent,
NumberEditor: '' as AnyComponent,
NumberPresenter: '' as AnyComponent,

View File

@ -42,7 +42,7 @@
"Facebook": "Facebook",
"HomepagePlaceholder": "https://jappleseed.com",
"Homepage": "Home page",
"SocialLinks": "Socail links",
"SocialLinks": "Social links",
"ViewActivity": "View activity",
"PersonAlreadyExists": "Contact already exists...",
"Status": "Status",

View File

@ -103,6 +103,8 @@
"Show": "Show",
"FilterArrayAll": "includes all",
"FilterArrayAny": "includes any",
"Or": "Or"
"Or": "Or",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copy to clipboard"
}
}

View File

@ -99,6 +99,8 @@
"Show": "Отображение",
"FilterArrayAll": "содержит все",
"FilterArrayAny": "содержит любое из",
"Or": "Или"
"Or": "Или",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Скопировать в буфер обмена"
}
}

View File

@ -0,0 +1,62 @@
<!--
// Copyright © 2023 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 type { IntlString } from '@hcengineering/platform'
import type { ButtonSize, ButtonKind } from '@hcengineering/ui'
import { Label, showPopup, eventToHTMLElement, Button, parseURL } from '@hcengineering/ui'
import HyperlinkEditorPopup from './HyperlinkEditorPopup.svelte'
export let placeholder: IntlString
export let value: string
export let onChange: (value: string) => void = () => {}
export let kind: ButtonKind | undefined = undefined
export let readonly = false
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'fit-content'
let shown: boolean = false
</script>
<Button
{kind}
{size}
{justify}
{width}
on:click={(ev) => {
if (!shown) {
showPopup(HyperlinkEditorPopup, { value, editable: !readonly }, eventToHTMLElement(ev), (res) => {
if (res === 'open') {
const url = parseURL(value)
if (url.startsWith('http://') || url.startsWith('https://')) {
window.open(url)
}
} else if (res !== undefined) {
value = res
onChange(value)
}
shown = false
})
}
}}
>
<svelte:fragment slot="content">
{#if value}
<span class="caption-color overflow-label pointer-events-none">{value}</span>
{:else}
<span class="content-dark-color pointer-events-none"><Label label={placeholder} /></span>
{/if}
</svelte:fragment>
</Button>

View File

@ -0,0 +1,143 @@
<!--
// Copyright © 2023 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 type { IntlString } from '@hcengineering/platform'
import { translate } from '@hcengineering/platform'
import { themeStore, Label } from '@hcengineering/ui'
import { Button, IconArrowRight, IconBlueCheck, IconClose } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import view from '../plugin'
export let value: string = ''
export let editable: boolean | undefined = true
const placeholder: IntlString = view.string.HyperlinkPlaceholder
const dispatch = createEventDispatcher()
let input: HTMLInputElement
let phTranslate: string
$: translate(placeholder, {}, $themeStore.language).then((tr) => (phTranslate = tr))
onMount(() => {
if (input) input.focus()
})
</script>
<div class="editor-container buttons-group xsmall-gap">
<div class="cover-channel show">
{#if editable}
<input
bind:this={input}
class="search"
type="text"
bind:value
placeholder={phTranslate}
style="width: 100%;"
on:keypress={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault()
ev.stopPropagation()
dispatch('close', value)
}
}}
on:change
/>
{:else if value}
<span class="overflow-label">{value}</span>
{:else}
<span class="content-dark-color"><Label label={placeholder} /></span>
{/if}
</div>
<Button
focusIndex={2}
kind={'ghost'}
size={'small'}
icon={IconClose}
disabled={value === ''}
on:click={() => {
if (input) {
value = ''
input.focus()
}
}}
/>
{#if editable}
<Button
id="channel-ok"
focusIndex={3}
kind={'ghost'}
size={'small'}
icon={IconBlueCheck}
on:click={() => {
dispatch('close', value)
}}
/>
{/if}
<Button
focusIndex={4}
kind={'ghost'}
size={'small'}
icon={IconArrowRight}
on:click={() => {
dispatch('update', value)
dispatch('close', 'open')
}}
/>
</div>
<style lang="scss">
.cover-channel {
position: relative;
min-width: 0;
min-height: 0;
&.show::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid transparent;
border-radius: 0.25rem;
opacity: 0.95;
}
&.show.copied::before {
border-color: var(--theme-divider-color);
}
&.show::after {
content: attr(data-tooltip);
position: absolute;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
top: 50%;
left: 50%;
width: calc(100% - 0.5rem);
text-align: center;
font-size: 0.75rem;
color: var(--theme-content-color);
transform: translate(-50%, -50%);
}
}
.editor-container {
padding: 0.5rem;
background-color: var(--theme-popup-color);
border: 1px solid var(--theme-popup-divider);
border-radius: 0.75rem;
box-shadow: var(--theme-popup-shadow);
}
</style>

View File

@ -44,6 +44,8 @@ import ValueFilter from './components/filter/ValueFilter.svelte'
import HTMLEditor from './components/HTMLEditor.svelte'
import HTMLPresenter from './components/HTMLPresenter.svelte'
import HyperlinkPresenter from './components/HyperlinkPresenter.svelte'
import HyperlinkEditor from './components/HyperlinkEditor.svelte'
import HyperlinkEditorPopup from './components/HyperlinkEditorPopup.svelte'
import IntlStringPresenter from './components/IntlStringPresenter.svelte'
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
@ -202,6 +204,8 @@ export default async (): Promise<Resources> => ({
StringEditor,
StringPresenter,
HyperlinkPresenter,
HyperlinkEditor,
HyperlinkEditorPopup,
NumberEditor,
NumberPresenter,
BooleanPresenter,

View File

@ -832,7 +832,9 @@ const view = plugin(viewId, {
Delete: '' as IntlString,
Then: '' as IntlString,
Or: '' as IntlString,
Subscribed: '' as IntlString
Subscribed: '' as IntlString,
HyperlinkPlaceholder: '' as IntlString,
CopyToClipboard: '' as IntlString
},
icon: {
Table: '' as Asset,