Merge pull request #2309 from hcengineering/tsk-257-copy-to-clipboard

Fix copying text to clipboard for Safari
This commit is contained in:
Alexander Onnikov 2022-10-17 14:23:18 +07:00 committed by GitHub
commit 7aeb3c19e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 102 additions and 59 deletions

View File

@ -702,9 +702,9 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: recruit.actionImpl.CopyToClipboard,
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
type: 'id'
textProvider: recruit.function.GetApplicationId
},
label: recruit.string.CopyId,
icon: recruit.icon.Application,
@ -723,9 +723,9 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: recruit.actionImpl.CopyToClipboard,
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
type: 'link'
textProvider: recruit.function.GetApplicationLink
},
label: recruit.string.CopyLink,
icon: recruit.icon.Application,
@ -744,9 +744,9 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: recruit.actionImpl.CopyToClipboard,
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
type: 'link'
textProvider: recruit.function.GetRecruitLink
},
label: recruit.string.CopyLink,
icon: recruit.icon.Application,

View File

@ -32,12 +32,16 @@ export default mergeIds(recruitId, recruit, {
CopyCandidateLink: '' as Ref<Action>
},
actionImpl: {
CreateOpinion: '' as ViewAction,
CopyToClipboard: '' as ViewAction
CreateOpinion: '' as ViewAction
},
category: {
Recruit: '' as Ref<ActionCategory>
},
function: {
GetApplicationId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetApplicationLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetRecruitLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
},
string: {
ApplicationShort: '' as IntlString,
ApplicationsShort: '' as IntlString,

View File

@ -1129,9 +1129,9 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: tracker.actionImpl.CopyToClipboard,
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
type: 'id'
textProvider: tracker.function.GetIssueId
},
label: tracker.string.CopyIssueId,
icon: tracker.icon.CopyID,
@ -1150,9 +1150,9 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: tracker.actionImpl.CopyToClipboard,
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
type: 'title'
textProvider: tracker.function.GetIssueTitle
},
label: tracker.string.CopyIssueTitle,
icon: tracker.icon.CopyBranch,
@ -1171,9 +1171,9 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: tracker.actionImpl.CopyToClipboard,
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
type: 'link'
textProvider: tracker.function.GetIssueLink
},
label: tracker.string.CopyIssueUrl,
icon: tracker.icon.CopyURL,

View File

@ -121,7 +121,8 @@
} else {
location.path.length = 4
}
await copyTextToClipboard(`${window.location.origin}${locationToUrl(location)}`)
const text = `${window.location.origin}${locationToUrl(location)}`
await copyTextToClipboard(text)
}
}

View File

@ -50,7 +50,7 @@ import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte'
import VacancyPresenter from './components/VacancyPresenter.svelte'
import recruit from './plugin'
import { copyToClipboard, getApplicationTitle } from './utils'
import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './utils'
import VacancyList from './components/VacancyList.svelte'
async function createOpinion (object: Doc): Promise<void> {
@ -254,8 +254,7 @@ async function noneApplicant (filter: Filter, onUpdate: () => void): Promise<Obj
export default async (): Promise<Resources> => ({
actionImpl: {
CreateOpinion: createOpinion,
CopyToClipboard: copyToClipboard
CreateOpinion: createOpinion
},
validator: {
ApplicantValidator: applicantValidator
@ -305,6 +304,9 @@ export default async (): Promise<Resources> => ({
ApplicationTitleProvider: getApplicationTitle,
HasActiveApplicant: hasActiveApplicant,
HasNoActiveApplicant: hasNoActiveApplicant,
NoneApplications: noneApplicant
NoneApplications: noneApplicant,
GetApplicationId: objectIdProvider,
GetApplicationLink: objectLinkProvider,
GetRecruitLink: objectLinkProvider
}
})

View File

@ -1,6 +1,6 @@
import core, { Doc, Ref, TxOperations } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { copyTextToClipboard, getClient } from '@hcengineering/presentation'
import { getClient } from '@hcengineering/presentation'
import { Applicant, Candidate } from '@hcengineering/recruit'
import { getPanelURI } from '@hcengineering/ui'
import view from '@hcengineering/view'
@ -19,23 +19,13 @@ export async function getApplicationTitle (client: TxOperations, ref: Ref<Doc>):
return `${label}-${object.number}`
}
export async function copyToClipboard (
object: Applicant | Candidate,
ev: Event,
{ type }: { type: string }
): Promise<void> {
export async function objectIdProvider (doc: Applicant | Candidate): Promise<string> {
const client = getClient()
let text: string
switch (type) {
case 'id':
text = await getApplicationTitle(client, object._id)
break
case 'link':
// TODO: fix when short link is available
text = `${window.location.href}#${getPanelURI(view.component.EditDoc, object._id, object._class, 'content')}`
break
default:
return
}
await copyTextToClipboard(text)
return await getApplicationTitle(client, doc._id)
}
export async function objectLinkProvider (doc: Applicant | Candidate): Promise<string> {
return await Promise.resolve(
`${window.location.href}#${getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')}`
)
}

View File

@ -59,7 +59,14 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte'
import { copyToClipboard, getIssueId, getIssueTitle, resolveLocation } from './issues'
import {
getIssueId,
getIssueTitle,
issueIdProvider,
issueLinkProvider,
issueTitleProvider,
resolveLocation
} from './issues'
import tracker from './plugin'
import SprintEditor from './components/sprints/SprintEditor.svelte'
@ -212,10 +219,12 @@ export default async (): Promise<Resources> => ({
await queryIssue(tracker.class.Issue, client, query, filter)
},
function: {
IssueTitleProvider: getIssueTitle
IssueTitleProvider: getIssueTitle,
GetIssueId: issueIdProvider,
GetIssueLink: issueLinkProvider,
GetIssueTitle: issueTitleProvider
},
actionImpl: {
CopyToClipboard: copyToClipboard,
EditWorkflowStatuses: editWorkflowStatuses
},
resolver: {

View File

@ -1,5 +1,5 @@
import { Doc, DocumentUpdate, Ref, RelatedDocument, TxOperations } from '@hcengineering/core'
import { copyTextToClipboard, getClient } from '@hcengineering/presentation'
import { getClient } from '@hcengineering/presentation'
import { Issue, Project, Sprint, Team, trackerId } from '@hcengineering/tracker'
import { getCurrentLocation, getPanelURI, Location } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
@ -31,24 +31,18 @@ export function generateIssuePanelUri (issue: Issue): string {
return getPanelURI(tracker.component.EditIssue, issue._id, issue._class, 'content')
}
export async function copyToClipboard (object: Issue, ev: Event, { type }: { type: string }): Promise<void> {
export async function issueIdProvider (doc: Doc): Promise<string> {
const client = getClient()
let text: string
switch (type) {
case 'id':
text = await getIssueTitle(client, object._id)
break
case 'title':
text = object.title
break
case 'link':
text = generateIssueShortLink(await getIssueTitle(client, object._id))
break
default:
return
}
return await getIssueTitle(client, doc._id)
}
await copyTextToClipboard(text)
export async function issueTitleProvider (doc: Issue): Promise<string> {
return await Promise.resolve(doc.title)
}
export async function issueLinkProvider (doc: Doc): Promise<string> {
const client = getClient()
return await getIssueTitle(client, doc._id).then(generateIssueShortLink)
}
export function generateIssueShortLink (issueId: string): string {

View File

@ -301,6 +301,9 @@ export default mergeIds(trackerId, tracker, {
IssueTemplatePresenter: '' as AnyComponent
},
function: {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
}
})

View File

@ -18,6 +18,41 @@ import view from './plugin'
import { FocusSelection, focusStore, previewDocument, SelectDirection, selectionStore } from './selection'
import { deleteObject } from './utils'
/**
* Action to be used for copying text to clipboard.
* In Safari a request to write to the clipboard must be triggered during a user gesture.
* A call to clipboard.write or clipboard.writeText outside the scope of a user
* gesture(such as "click" or "touch" event handlers) will result in the immediate
* rejection of the promise returned by the API call.
* https://webkit.org/blog/10855/async-clipboard-api/
*
* * Require props:
* - textProvider - a function that provides text to be copied.
* - props - additional text provider props.
*/
async function CopyTextToClipboard (
doc: Doc,
evt: Event,
props: {
textProvider: Resource<(doc: Doc, props?: Record<string, any>) => Promise<string>>
props?: Record<string, any>
}
): Promise<void> {
const getText = await getResource(props.textProvider)
try {
// Safari specific behavior
// see https://bugs.webkit.org/show_bug.cgi?id=222262
const clipboardItem = new ClipboardItem({
'text/plain': getText(doc, props.props)
})
await navigator.clipboard.write([clipboardItem])
} catch {
// Fallback to default clipboard API implementation
const text = await getText(doc, props.props)
await navigator.clipboard.writeText(text)
}
}
function Delete (object: Doc): void {
showPopup(
MessageBox,
@ -334,6 +369,7 @@ async function getPopupAlignment (
* @public
*/
export const actionImpl = {
CopyTextToClipboard,
Delete,
Move,
MoveUp,

View File

@ -503,6 +503,10 @@ const view = plugin(viewId, {
PositionElementAlignment: '' as Resource<(e?: Event) => PopupAlignment | undefined>
},
actionImpl: {
CopyTextToClipboard: '' as ViewAction<{
textProvider: Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
props?: Record<string, any>
}>,
UpdateDocument: '' as ViewAction<{
key: string
value: any