Commit 578022b8 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: replace slug field with interactive permalink component

- create permalink-edit web component with slug editing and permalink copy functionalities
- add
@github/clipboard-copy-element
- update npm packages
- replace vscode extension lit-html with
lit-plugin to get css intellisense
parent 230e139e
......@@ -25,7 +25,6 @@
}
},
"extensions": [
"bierner.lit-html",
"bmewburn.vscode-intelephense-client",
"bradlc.vscode-tailwindcss",
"breezelin.phpstan",
......@@ -37,6 +36,7 @@
"kasik96.latte",
"mikestead.dotenv",
"naumovs.color-highlight",
"runem.lit-plugin",
"streetsidesoftware.code-spell-checker",
"stylelint.vscode-stylelint",
"wayou.vscode-todo-highlight"
......
......@@ -20,17 +20,17 @@ if (! function_exists('render_page_links')) {
{
$pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
$links .= anchor(route_to('map'), lang('Page.map'), [
'class' => 'px-2 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
}
......
......@@ -16,6 +16,9 @@ return [
'more' => 'More',
'no_data' => 'No data found!',
'close' => 'Close',
'edit' => 'Edit',
'copy' => 'Copy',
'copied' => 'Copied!',
'home' => 'Home',
'explicit' => 'Explicit',
'mediumDate' => '{0,date,medium}',
......
......@@ -67,8 +67,7 @@ return [
'title' => 'Title',
'title_hint' =>
'Should contain a clear and concise episode name. Do not specify the episode or season numbers here.',
'slug' => 'Slug',
'slug_hint' => 'Used for generating the episode URL.',
'permalink' => 'Permalink',
'season_number' => 'Season',
'episode_number' => 'Episode',
'type' => [
......
......@@ -18,7 +18,7 @@ return [
'delete' => 'Delete page',
'form' => [
'title' => 'Title',
'slug' => 'Slug',
'permalink' => 'Permalink',
'content' => 'Content',
'submit_create' => 'Create page',
'submit_edit' => 'Save',
......
......@@ -16,6 +16,9 @@ return [
'more' => 'Plus',
'no_data' => 'Aucune donnée trouvée !',
'close' => 'Fermer',
'edit' => 'Modifier',
'copy' => 'Copier',
'copied' => 'Copié !',
'home' => 'Accueil',
'explicit' => 'Explicite',
'mediumDate' => '{0,date,medium}',
......
......@@ -67,8 +67,7 @@ return [
'title' => 'Titre',
'title_hint' =>
'Doit contenir un titre d’épisode clair et concis. Ne précisez ici aucun numéro de saison ou d’épisode.',
'slug' => 'Identifiant',
'slug_hint' => 'Utilisé pour générer l’adresse de l’épisode.',
'permalink' => 'Lien permanent',
'season_number' => 'Saison',
'episode_number' => 'Épisode',
'type' => [
......@@ -144,7 +143,7 @@ return [
'submit' => 'Publier',
'submit_edit' => 'Modifier la publication',
'cancel_publication' => 'Annuler la publication',
'message_warning' => 'Vous n’avez pas saisi de message pour l’annonce de votre épisode !',
'message_warning' => 'Vous n’avez pas saisi de message pour l’annonce de votre épisode!',
'message_warning_hint' => 'Ajouter un message augmente l’engagement social, menant à une meilleure visibilité pour votre épisode.',
'message_warning_submit' => 'Publish quand même',
],
......
......@@ -18,7 +18,7 @@ return [
'delete' => 'Supprimer la page',
'form' => [
'title' => 'Titre',
'slug' => 'Identifiant',
'permalink' => 'Lien permanent',
'content' => 'Contenu',
'submit_create' => 'Créer la page',
'submit_edit' => 'Enregistrer',
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M6 4v4h12V4h2.007c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.994.994 0 0 1 3 21.007V4.993C3 4.445 3.445 4 3.993 4H6zm2-2h8v4H8V2z"/>
</g>
</svg>
......@@ -7,6 +7,7 @@ import Dropdown from "./modules/Dropdown";
import "./modules/markdown-preview";
import "./modules/markdown-write-preview";
import MultiSelect from "./modules/MultiSelect";
import "./modules/permalink-edit";
import PublishMessageWarning from "./modules/PublishMessageWarning";
import Select from "./modules/Select";
import SidebarToggler from "./modules/SidebarToggler";
......
......@@ -29,6 +29,7 @@ const Slugify = (): void => {
if (title && slug) {
title.addEventListener("input", () => {
slug.value = slugify(title.value);
slug.dispatchEvent(new Event("change"));
});
}
};
......
import "@github/clipboard-copy-element";
import { css, html, LitElement, TemplateResult } from "lit";
import {
customElement,
property,
query,
queryAssignedNodes,
state,
} from "lit/decorators.js";
@customElement("permalink-edit")
export class PermalinkEdit extends LitElement {
@queryAssignedNodes("domain", true)
_domain!: NodeListOf<HTMLSpanElement>;
@queryAssignedNodes("slug-input", true)
_slugInput!: NodeListOf<HTMLInputElement>;
@query("clipboard-copy")
_clipboardCopy!: any;
@property({ attribute: "edit-label" })
editLabel = "Edit";
@property({ attribute: "copy-label" })
copyLabel = "Copy";
@state()
isEditable = false;
@state()
permalink = "";
@state()
slugInputEvents = [
{
name: "change",
onEvent: (): void => {
this.setPermalink();
},
},
{
name: "focus",
onEvent: (): void => {
this.editSlug();
},
},
{
name: "focusin",
onEvent: (event: Event): void => {
setTimeout(() => {
(event.target as HTMLInputElement).selectionStart = (
event.target as HTMLInputElement
).selectionEnd = 10000;
}, 0);
},
},
{
name: "focusout",
onEvent: (): void => {
this.stopEdit();
},
},
];
connectedCallback(): void {
super.connectedCallback();
}
firstUpdated(): void {
// set permalink value
this.setPermalink();
this._clipboardCopy.addEventListener("clipboard-copy", (event: Event) => {
const notice = (event.target as HTMLDivElement).querySelector(
".notice"
) as HTMLSpanElement;
if (notice) {
notice.hidden = false;
setTimeout(() => {
notice.hidden = true;
}, 1000);
}
});
this._slugInput[0].readOnly = !this.isEditable;
this.slugInputEvents.forEach((slugInputEvent) => {
this._slugInput[0].addEventListener(
slugInputEvent.name,
slugInputEvent.onEvent
);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.slugInputEvents.forEach((slugInputEvent) => {
this._slugInput[0].removeEventListener(
slugInputEvent.name,
slugInputEvent.onEvent
);
});
this._clipboardCopy.removeEventListener(
"clipboard-copy",
(event: Event) => {
const notice = (event.target as HTMLDivElement).querySelector(
".notice"
) as HTMLSpanElement;
if (notice) {
notice.hidden = false;
setTimeout(() => {
notice.hidden = true;
}, 1000);
}
}
);
}
editSlug(): void {
this.isEditable = true;
this._slugInput[0].readOnly = !this.isEditable;
this._slugInput[0].focus();
}
stopEdit(): void {
this.isEditable = false;
this._slugInput[0].readOnly = !this.isEditable;
}
setPermalink(): void {
this.permalink = this._domain[0].innerHTML + this._slugInput[0].value;
}
static styles = css`
::slotted(input[slot="slug-input"][readonly]) {
background-color: transparent !important;
border-color: transparent !important;
padding-left: 0 !important;
margin-left: -0.25rem !important;
font-weight: 600;
}
::slotted([slot="domain"]) {
margin-right: 0.25rem;
}
button,
clipboard-copy {
background: transparent;
border: none;
padding: 0.25rem;
cursor: pointer;
}
button svg,
clipboard-copy svg {
opacity: 0.6;
font-size: 1.25rem;
}
button:hover svg,
clipboard-copy:hover svg {
opacity: 1;
}
clipboard-copy {
position: relative;
}
.notice {
position: absolute;
background-color: black;
color: #ffffff;
bottom: -1rem;
right: 0;
font-size: 0.75rem;
padding: 0 0.25rem;
}
`;
render(): TemplateResult<1> {
return html`<slot name="domain"></slot><slot name="slug-input"></slot>${this
.isEditable
? ""
: html`<button @click="${this.editSlug}" title="${this.editLabel}">
<svg
viewBox="0 0 24 24"
fill="currentColor"
width="1em"
height="1em"
>
<g>
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M7.243 18H3v-4.243L14.435 2.322a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L7.243 18zM3 20h18v2H3v-2z"
/>
</g>
</svg>
</button> `}<clipboard-copy
.value="${this.permalink}"
title="${this.copyLabel}"
><svg viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em">
<g>
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M6 4v4h12V4h2.007c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.994.994 0 0 1 3 21.007V4.993C3 4.445 3.445 4 3.993 4H6zm2-2h8v4H8V2z"
/>
</g>
</svg>
<span class="notice" hidden>Copied!</span></clipboard-copy
>`;
}
}
......@@ -77,19 +77,22 @@
]) ?>
<?= form_label(
lang('Episode.form.slug'),
lang('Episode.form.permalink'),
'slug',
[],
lang('Episode.form.slug_hint'),
) ?>
<?= form_input([
<permalink-edit class="inline-flex items-center mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
<span slot="domain"><?= base_url('/@'. $podcast->handle . '/episodes' ) . '/' ?></span>
<?= form_input([
'id' => 'slug',
'name' => 'slug',
'class' => 'form-input mb-4',
'class' => 'form-input flex-1 w-0 text-xs',
'value' => old('slug'),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
'slot' => 'slug-input'
]) ?>
</permalink-edit>
<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
<div class="flex flex-col flex-1">
......
......@@ -84,19 +84,22 @@
]) ?>
<?= form_label(
lang('Episode.form.slug'),
lang('Episode.form.permalink'),
'slug',
[],
lang('Episode.form.slug_hint'),
) ?>
<?= form_input([
<permalink-edit class="inline-flex items-center mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
<span slot="domain"><?= base_url('/@'. $podcast->handle . '/episodes' ) . '/' ?></span>
<?= form_input([
'id' => 'slug',
'name' => 'slug',
'class' => 'form-input mb-4',
'class' => 'form-input flex-1 w-0 text-xs',
'value' => old('slug', $episode->slug),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
'slot' => 'slug-input'
]) ?>
</permalink-edit>
<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
<div class="flex flex-col flex-1">
......
......@@ -26,15 +26,23 @@
'data-slugify' => 'title',
]) ?>
<?= form_label(lang('Page.form.slug'), 'slug', ['class' => 'max-w-sm']) ?>
<?= form_label(
lang('Page.form.permalink'),
'slug',
[],
) ?>
<permalink-edit class="inline-flex items-center w-full max-w-sm mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
<span slot="domain" class="flex-shrink-0"><?= base_url('pages' ) . '/' ?></span>
<?= form_input([
'id' => 'slug',
'name' => 'slug',
'class' => 'form-input mb-4 max-w-sm',
'class' => 'form-input flex-1 w-0 text-xs',
'value' => old('slug'),
'required' => 'required',
'data-slugify' => 'slug',
'slot' => 'slug-input',
]) ?>
</permalink-edit>
<div class="mb-4">
<?= form_label(lang('Page.form.content'), 'content') ?>
......
......@@ -24,17 +24,25 @@
'value' => old('title', $page->title),
'required' => 'required',
'data-slugify' => 'title',
'slot' => 'slug-input',
]) ?>
<?= form_label(lang('Page.form.slug'), 'slug', ['class' => 'max-w-sm']) ?>
<?= form_label(
lang('Page.form.permalink'),
'slug',
[],
) ?>
<permalink-edit class="inline-flex items-center max-w-sm mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
<span slot="domain" class="flex-shrink-0"><?= base_url('pages') . '/' ?></span>
<?= form_input([
'id' => 'slug',
'name' => 'slug',
'class' => 'form-input mb-4 max-w-sm',
'class' => 'form-input flex-1 w-0 text-xs',
'value' => old('slug', $page->slug),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
</permalink-edit>
<div class="mb-4">
<?= form_label(lang('Page.form.content'), 'content') ?>
......
......@@ -9,11 +9,12 @@
"version": "1.0.0-alpha.80",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@amcharts/amcharts4": "^4.10.20",
"@amcharts/amcharts4": "^4.10.21",
"@amcharts/amcharts4-geodata": "^4.1.21",
"@github/clipboard-copy-element": "^1.1.2",
"@github/markdown-toolbar-element": "^1.5.1",
"@github/time-elements": "^3.1.2",
"@popperjs/core": "^2.9.2",
"@popperjs/core": "^2.9.3",
"@vime/core": "^5.0.33",
"choices.js": "^9.0.1",
"flatpickr": "^4.6.9",
......@@ -28,25 +29,23 @@
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/exec": "^5.0.0",
"@semantic-release/git": "^9.0.0",
"@semantic-release/gitlab": "^6.2.1",
"@semantic-release/gitlab": "^6.2.2",
"@tailwindcss/forms": "^0.3.3",
"@tailwindcss/line-clamp": "^0.2.1",
"@tailwindcss/typography": "^0.4.1",
"@types/leaflet": "^1.7.5",
"@types/marked": "^2.0.4",
"@types/prosemirror-markdown": "^1.5.2",
"@types/prosemirror-view": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^4.28.5",
"@typescript-eslint/parser": "^4.28.5",
"@typescript-eslint/eslint-plugin": "^4.29.1",
"@typescript-eslint/parser": "^4.29.1",
"cross-env": "^7.0.3",
"cssnano": "^5.0.7",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.31.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"husky": "^7.0.1",
"is-ci": "^3.0.0",
"lint-staged": "^11.1.1",
"lint-staged": "^11.1.2",
"lit": "^2.0.0-rc.2",
"postcss-import": "^14.0.2",
"postcss-preset-env": "^6.7.0",
......@@ -1035,6 +1034,11 @@
"resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz",
"integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA=="
},
"node_modules/@github/clipboard-copy-element": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@github/clipboard-copy-element/-/clipboard-copy-element-1.1.2.tgz",
"integrity": "sha512-L6CMrcA5we0udafvoSuRCE/Ci/3xrLWKYRGup2IlhxF771bQYsQ2EB1of182pI8ZWM4oxgwzu37+igMeoZjN/A=="
},
"node_modules/@github/markdown-toolbar-element": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-1.5.3.tgz",
......@@ -1941,12 +1945,6 @@
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
"dev": true
},
"node_modules/@types/highlight.js": {
"version": "9.12.4",
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
"dev": true
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
......@@ -1977,23 +1975,6 @@
"@types/geojson": "*"
}
},
"node_modules/@types/linkify-it": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.0.tgz",
"integrity": "sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==",
"dev": true
},
"node_modules/@types/markdown-it": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.1.tgz",
"integrity": "sha512-mHfT8j/XkPb1uLEfs0/C3se6nd+webC2kcqcy8tgcVr0GDEONv/xaQzAN+aQvkxQXk/jC0Q6mPS+0xhFwRF35g==",
"dev": true,
"dependencies": {
"@types/highlight.js": "^9.7.0",
"@types/linkify-it": "*",
"@types/mdurl": "*"
}
},
"node_modules/@types/marked": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.5.tgz",
......@@ -2009,12 +1990,6 @@
"@types/unist": "*"
}
},
"node_modules/@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
"dev": true
},
"node_modules/@types/minimist": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz",
......@@ -2033,18 +2008,13 @@
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
"dev": true
},
"node_modules/@types/orderedmap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/orderedmap/-/orderedmap-1.0.0.tgz",
"integrity": "sha512-dxKo80TqYx3YtBipHwA/SdFmMMyLCnP+5mkEqN0eMjcTBzHkiiX0ES118DsjDBjvD+zeSsSU9jULTZ+frog+Gw==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
<<<<<<< HEAD
"node_modules/@types/prosemirror-markdown": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/prosemirror-markdown/-/prosemirror-markdown-1.5.4.tgz",
......@@ -2095,6 +2065,8 @@
"@types/prosemirror-transform": "*"
}
},
=======
>>>>>>> c94a163 (feat: replace slug field with interactive permalink component)
"node_modules/@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
......@@ -20247,6 +20219,11 @@
"resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz",
"integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA=="
},
"@github/clipboard-copy-element": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@github/clipboard-copy-element/-/clipboard-copy-element-1.1.2.tgz",
"integrity": "sha512-L6CMrcA5we0udafvoSuRCE/Ci/3xrLWKYRGup2IlhxF771bQYsQ2EB1of182pI8ZWM4oxgwzu37+igMeoZjN/A=="
},
"@github/markdown-toolbar-element": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-1.5.3.tgz",
......@@ -20975,12 +20952,6 @@
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
"dev": true
},
"@types/highlight.js": {
"version": "9.12.4",
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
"dev": true
},