-
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1851 from NullVoxPopuli/share-menu
Share menu
- Loading branch information
Showing
8 changed files
with
370 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
/* focus */ | ||
button[data-share-button], | ||
dialog.preem input { | ||
--tw-ring-color: var(--ember-brand); | ||
|
||
&:focus-visible, &:focus { | ||
outline: 2px solid transparent; | ||
outsilen-offset: 2px; | ||
border-color: transparent; | ||
} | ||
&:focus { | ||
--tw-ring-offset-shadow: | ||
var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); | ||
--tw-ring-shadow: | ||
var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); | ||
box-shadow: | ||
var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); | ||
} | ||
} | ||
|
||
dialog.preem { | ||
.error { | ||
border: 2px solid #622; | ||
border-radius: 0.25rem; | ||
padding: 0.25rem; | ||
background: #fee; | ||
} | ||
} | ||
|
||
|
||
button[data-share-button] { | ||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | ||
transition-duration: 150ms; | ||
transition-property: all; | ||
--tw-text-opacity: 1; | ||
color: rgb(255 255 255 / var(--tw-text-opacity)); | ||
padding: 0.25rem 0.5rem; | ||
/** | ||
* TODO: can this be nested automatically? | ||
*/ | ||
border-radius: 0.25rem; | ||
cursor: pointer; | ||
display: flex; | ||
align-items: center; | ||
gap: 0.5rem; | ||
} | ||
|
||
|
||
.preem__tip { | ||
--tw-bg-opacity: 1; | ||
padding: 1rem; | ||
background: white; | ||
background: rgb(240 249 255 / var(--tw-bg-opacity)); | ||
color: rgb(50, 60, 100); | ||
position: relative; | ||
border-radius: 0.25rem; | ||
|
||
.preem__tip__bulb { | ||
position: absolute; | ||
left: 1rem; | ||
top: 1rem; | ||
text-shadow: 1px 2px 1px rgba(0,0,0,0.2); | ||
font-size: 1.5rem; | ||
} | ||
.preem__tip__text { | ||
padding-left: 2rem; | ||
padding-right: 1.5rem; | ||
} | ||
} | ||
dialog.preem { | ||
border-radius: 0.25rem; | ||
animation: var(--animation-slide-in-up), var(--animation-fade-in); | ||
animation-timing-function: var(--ease-out-5); | ||
animation-duration: 0.2s; | ||
} | ||
dialog.preem::backdrop { | ||
backdrop-filter: blur(1px); | ||
} | ||
dialog.preem header { | ||
display: flex; | ||
justify-content: space-between; | ||
padding: 1rem; | ||
align-items: center; | ||
border-bottom: 1px solid #333; | ||
} | ||
dialog.preem h2 { | ||
margin: 0 !important; | ||
} | ||
|
||
dialog.preem main { | ||
padding: 2rem; | ||
padding-bottom: 1rem; | ||
max-width: 500px; | ||
display: grid; | ||
gap: 1rem; | ||
|
||
.field { | ||
display: grid; | ||
gap: 0.5rem; | ||
input { | ||
width: 100%; | ||
} | ||
.field-input { | ||
display: flex; | ||
gap: 0.5rem; | ||
} | ||
} | ||
input { | ||
padding: 0.25rem 0.5rem; | ||
border: 1px solid #333; | ||
border-radius: 0.25rem; | ||
color: #444; | ||
} | ||
} | ||
|
||
dialog.preem form { | ||
} | ||
dialog.preem .inline-mini-form { | ||
display: grid; | ||
align-items: end; | ||
grid-auto-flow: column; | ||
grid-template-columns: 1fr min-content; | ||
|
||
[type="submit"] { | ||
margin-left: 0.5rem; | ||
} | ||
} | ||
|
||
dialog.preem footer, | ||
dialog.preem form { | ||
button { | ||
color: white; | ||
border-radius: 0.25rem; | ||
padding: 0.25rem 0.5rem; | ||
background: var(--code-bg); | ||
border: 1px solid var(--horizon-border); | ||
|
||
&.cancel { | ||
color: black; | ||
background: #eee; | ||
border-color: #aaa; | ||
} | ||
} | ||
|
||
button:focus { | ||
outline: 2px solid transparent; | ||
outline-offset: 2px; | ||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) | ||
var(--tw-ring-offset-color); | ||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) | ||
var(--tw-ring-color); | ||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); | ||
} | ||
|
||
button:focus-visible { | ||
outline: 2px solid transparent; | ||
outline-offset: 2px; | ||
} | ||
|
||
button:hover { | ||
opacity: 0.9; | ||
} | ||
} | ||
|
||
dialog.preem footer { | ||
padding: 1rem 2rem; | ||
|
||
.right { | ||
display: grid; | ||
justify-content: end; | ||
} | ||
|
||
.buttons { | ||
display: flex; | ||
gap: 1rem; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import './share.css'; | ||
|
||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { array, fn } from '@ember/helper'; | ||
import { on } from '@ember/modifier'; | ||
import { service } from '@ember/service'; | ||
|
||
import FaIcon from '@fortawesome/ember-fontawesome/components/fa-icon'; | ||
// @ts-expect-error womp types | ||
import { focusTrap } from 'ember-focus-trap'; | ||
import { Modal } from 'ember-primitives/components/dialog'; | ||
|
||
import { shortenUrl } from 'limber/utils/editor-text'; | ||
|
||
import { FlatButton } from './help'; | ||
|
||
import type { TOC } from '@ember/component/template-only'; | ||
import type RouterService from '@ember/routing/router-service'; | ||
|
||
const { Boolean } = globalThis; | ||
|
||
export class Share extends Component { | ||
<template> | ||
<Modal as |m|> | ||
<button data-share-button type="button" {{on "click" m.open}}> | ||
Share | ||
<FaIcon @icon="share-from-square" @prefix="fas" /> | ||
</button> | ||
<m.Dialog class="preem" {{focusTrap isActive=m.isOpen}}> | ||
<header><h2>Share</h2> | ||
|
||
<FlatButton {{on "click" m.close}} aria-label="close this share modal"> | ||
<FaIcon @size="xs" @icon="xmark" class="aspect-square" /> | ||
</FlatButton> | ||
</header> | ||
<form {{on "submit" this.handleSubmit}}> | ||
<main> | ||
{{#if this.error}} | ||
<div class="error">{{this.error}}</div> | ||
{{/if}} | ||
<div class="inline-mini-form"> | ||
<ReadonlyField | ||
@label="shortened URL" | ||
@value={{this.shortUrl}} | ||
@copyable={{Boolean this.shortUrl}} | ||
placeholder="Click 'Create'" | ||
/> | ||
|
||
{{#unless this.shortUrl}} | ||
<button type="submit">Create</button> | ||
{{/unless}} | ||
</div> | ||
<Tip> | ||
<KeyCombo @keys={{array "Ctrl" "S"}} @mac={{array "Command" "S"}} /> | ||
will copy a shortened URL to your clipboard.</Tip> | ||
</main> | ||
|
||
<footer> | ||
<div class="right"> | ||
<div class="buttons"> | ||
<button type="button" class="cancel" {{on "click" m.close}}>Close</button> | ||
{{#unless this.shortUrl}} | ||
<button type="submit">Create Link</button> | ||
{{/unless}} | ||
</div> | ||
</div> | ||
</footer> | ||
</form> | ||
</m.Dialog> | ||
</Modal> | ||
</template> | ||
|
||
@service declare router: RouterService; | ||
|
||
@tracked shortUrl: string | undefined; | ||
@tracked error: string | undefined; | ||
|
||
toClipboard = async () => { | ||
let url = location.origin + this.router.currentURL; | ||
|
||
try { | ||
url = await shortenUrl(url); | ||
} catch (e) { | ||
console.error(`Could not shorten the URL`); | ||
console.error(e); | ||
throw e; | ||
} | ||
|
||
await navigator.clipboard.writeText(url); | ||
}; | ||
|
||
handleSubmit = async (event: SubmitEvent) => { | ||
event.preventDefault(); | ||
this.error = undefined; | ||
|
||
let href = window.location.href; | ||
|
||
if (!href.includes('glimdown.com')) { | ||
if (href.includes('localhost')) { | ||
this.error = "You're on localhost, silly, here is a fake error with fake URL"; | ||
this.shortUrl = 'https://share.glimdown.com/something'; | ||
} else { | ||
this.error = 'This is only supported on glimdown.com'; | ||
} | ||
|
||
return; | ||
} | ||
|
||
try { | ||
await this.toClipboard(); | ||
} catch { | ||
// TODO: Toast message | ||
} | ||
}; | ||
} | ||
|
||
async function writeToClipboard(text: string) { | ||
await navigator.clipboard.writeText(text); | ||
} | ||
|
||
// "with copy" / @copyable={{true}}? | ||
const ReadonlyField: TOC<{ | ||
Element: HTMLInputElement; | ||
Args: { | ||
label: string; | ||
value: string | undefined; | ||
copyable?: boolean | undefined; | ||
}; | ||
}> = <template> | ||
<span class="field"> | ||
<label for="share-copy">{{@label}}</label> | ||
<span class="field-input"> | ||
<input value={{@value}} name="share-copy" readonly ...attributes /> | ||
{{#if @copyable}} | ||
<button type="button" {{on "click" (fn writeToClipboard @value)}}>Copy</button> | ||
{{/if}} | ||
</span> | ||
</span> | ||
</template>; | ||
|
||
const isLast = (collection: unknown[], index: number) => index === collection.length - 1; | ||
const isNotLast = (collection: unknown[], index: number) => !isLast(collection, index); | ||
const isMac = navigator.userAgent.indexOf('Mac OS') >= 0; | ||
const getKeys = (keys: string[], mac: string[]) => (isMac ? mac ?? keys : keys); | ||
|
||
const KeyCombo: TOC<{ | ||
Args: { | ||
keys: string[]; | ||
mac: string[]; | ||
}; | ||
}> = <template> | ||
<span class="preem__key-combination"> | ||
{{#let (getKeys @keys @mac) as |keys|}} | ||
{{#each keys as |key i|}} | ||
<Key>{{key}}</Key> | ||
{{#if (isNotLast @keys i)}} | ||
<span class="preem__key-combination__separator">+</span> | ||
{{/if}} | ||
{{/each}} | ||
{{/let}} | ||
</span> | ||
</template>; | ||
|
||
const Key: TOC<{ | ||
Blocks: { default?: [] }; | ||
}> = <template> | ||
<span class="preem-key">{{yield}}</span> | ||
</template>; | ||
|
||
const Tip: TOC<{ | ||
Blocks: { | ||
default: []; | ||
}; | ||
}> = <template> | ||
<div class="preem__tip"><span class="preem__tip__bulb">💡</span><p | ||
class="preem__tip__text" | ||
>{{yield}}</p></div> | ||
</template>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.