fix(md-editor): build new markdown editor with lit + github/markdown-toolbar-element
- create markdown-write-preview + markdown-preview webcomponents using lit - create form_markdown_editor helper form component - simplify form_dropdown and form_multiselect components - fix partner fields display fixes #93, #94, #120
|
@ -25,19 +25,20 @@
|
|||
}
|
||||
},
|
||||
"extensions": [
|
||||
"mikestead.dotenv",
|
||||
"bierner.lit-html",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"naumovs.color-highlight",
|
||||
"heybourn.headwind",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"esbenp.prettier-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"jamesbirtles.svelte-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"eamodio.gitlens",
|
||||
"breezelin.phpstan",
|
||||
"kasik96.latte"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"esbenp.prettier-vscode",
|
||||
"heybourn.headwind",
|
||||
"jamesbirtles.svelte-vscode",
|
||||
"kasik96.latte",
|
||||
"mikestead.dotenv",
|
||||
"naumovs.color-highlight",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"stylelint.vscode-stylelint",
|
||||
"wayou.vscode-todo-highlight"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -157,17 +157,9 @@ if (! function_exists('form_multiselect')) {
|
|||
): string {
|
||||
$defaultExtra = [
|
||||
'data-class' => $customExtra['class'],
|
||||
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
|
||||
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
|
||||
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
'multiple' => 'multiple',
|
||||
];
|
||||
$extra = stringify_attributes(array_merge($defaultExtra, $customExtra));
|
||||
|
||||
if (stripos($extra, 'multiple') === false) {
|
||||
$extra .= ' multiple="multiple"';
|
||||
}
|
||||
$extra = array_merge($defaultExtra, $customExtra);
|
||||
|
||||
return form_dropdown($name, $options, $selected, $extra);
|
||||
}
|
||||
|
@ -179,43 +171,31 @@ if (! function_exists('form_dropdown')) {
|
|||
/**
|
||||
* Drop-down Menu (based on html select tag)
|
||||
*
|
||||
* @param array<string, mixed>|string $data
|
||||
* @param array<string, string> $options
|
||||
* @param string|string[] $selected
|
||||
* @param array<string, mixed>|string $extra
|
||||
* @param array<string, mixed> $options
|
||||
* @param string[] $selected
|
||||
* @param array<string, mixed> $customExtra
|
||||
*/
|
||||
function form_dropdown(
|
||||
string | array $data = '',
|
||||
string $name = '',
|
||||
array $options = [],
|
||||
string | array $selected = [],
|
||||
string | array $extra = ''
|
||||
array $selected = [],
|
||||
array $customExtra = []
|
||||
): string {
|
||||
$defaults = [];
|
||||
if (is_array($data)) {
|
||||
if (isset($data['selected'])) {
|
||||
$selected = $data['selected'];
|
||||
unset($data['selected']); // select tags don't have a selected attribute
|
||||
}
|
||||
if (isset($data['options'])) {
|
||||
$options = $data['options'];
|
||||
unset($data['options']); // select tags don't use an options attribute
|
||||
}
|
||||
} else {
|
||||
$defaults = [
|
||||
'name' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
if (! is_array($selected)) {
|
||||
$selected = [$selected];
|
||||
}
|
||||
if (! is_array($options)) {
|
||||
$options = [$options];
|
||||
}
|
||||
$defaultExtra = [
|
||||
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
|
||||
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
|
||||
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
$extra = array_merge($defaultExtra, $customExtra);
|
||||
$defaults = [
|
||||
'name' => $name,
|
||||
];
|
||||
|
||||
// standardize selected as strings, like the option keys will be.
|
||||
foreach ($selected as $key => $item) {
|
||||
$selected[$key] = (string) $item;
|
||||
$selected[$key] = $item;
|
||||
}
|
||||
|
||||
$placeholderOption = '';
|
||||
|
@ -230,11 +210,10 @@ if (! function_exists('form_dropdown')) {
|
|||
|
||||
$extra = stringify_attributes($extra);
|
||||
$multiple = (count($selected) > 1 && stripos($extra, 'multiple') === false) ? ' multiple="multiple"' : '';
|
||||
$form = '<select ' . rtrim(parse_form_attributes($data, $defaults)) . $extra . $multiple . ">\n";
|
||||
$form = '<select ' . rtrim(parse_form_attributes($name, $defaults)) . $extra . $multiple . ">\n";
|
||||
$form .= $placeholderOption;
|
||||
|
||||
foreach ($options as $key => $val) {
|
||||
$key = (string) $key;
|
||||
if (is_array($val)) {
|
||||
if ($val === []) {
|
||||
continue;
|
||||
|
@ -257,4 +236,80 @@ if (! function_exists('form_dropdown')) {
|
|||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('form_editor')) {
|
||||
/**
|
||||
* Markdown editor
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed>|string $extra
|
||||
*/
|
||||
function form_markdown_editor(array $data = [], string $value = '', string | array $extra = ''): string
|
||||
{
|
||||
$editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600';
|
||||
if (array_key_exists('class', $data) && $data['class'] !== '') {
|
||||
$editorClass .= ' ' . $data['class'];
|
||||
unset($data['class']);
|
||||
}
|
||||
|
||||
$data['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
|
||||
|
||||
return '<div class="' . $editorClass . '">' .
|
||||
'<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' .
|
||||
'<markdown-write-preview for="' . $data['id'] . '" class="relative inline-flex h-8">' .
|
||||
'<button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
|
||||
'Common.forms.editor.write'
|
||||
) . '</button>' .
|
||||
'<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
|
||||
'Common.forms.editor.preview'
|
||||
) . '</button>' .
|
||||
'</markdown-write-preview>' .
|
||||
'<markdown-toolbar for="' . $data['id'] . '" class="flex gap-4 px-2 py-1">' .
|
||||
'<div class="inline-flex text-2xl gap-x-1">' .
|
||||
'<md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'heading'
|
||||
) . '</md-header>' .
|
||||
'<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'bold'
|
||||
) . '</md-bold>' .
|
||||
'<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'italic'
|
||||
) . '</md-italic>' .
|
||||
'</div>' .
|
||||
'<div class="inline-flex text-2xl gap-x-1">' .
|
||||
'<md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'list-unordered'
|
||||
) . '</md-unordered-list>' .
|
||||
'<md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'list-ordered'
|
||||
) . '</md-ordered-list>' .
|
||||
'</div>' .
|
||||
'<div class="inline-flex text-2xl gap-x-1">' .
|
||||
'<md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'quote'
|
||||
) . '</md-quote>' .
|
||||
'<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'link'
|
||||
) . '</md-link>' .
|
||||
'<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
|
||||
'image-add'
|
||||
) . '</md-image>' .
|
||||
'</div>' .
|
||||
'</markdown-toolbar>' .
|
||||
'</header>' .
|
||||
'<div class="relative">' .
|
||||
form_textarea($data, $value, $extra) .
|
||||
'<markdown-preview for="' . $data['id'] . '" class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white"></markdown-preview>' .
|
||||
'</div>' .
|
||||
'<footer class="flex px-2 py-1 bg-gray-100 border-t">' .
|
||||
'<a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-gray-500 hover:text-gray-700" target="_blank" rel="noopener noreferrer">' . icon(
|
||||
'markdown',
|
||||
'mr-1 text-lg text-gray-400'
|
||||
) . lang('Common.forms.editor.help') . '</a>' .
|
||||
'</footer>' .
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
|
@ -24,6 +24,11 @@ return [
|
|||
'pageInfo' => 'Page {currentPage} out of {pageCount}',
|
||||
'go_back' => 'Go back',
|
||||
'forms' => [
|
||||
'editor' => [
|
||||
'write' => 'Write',
|
||||
'preview' => 'Preview',
|
||||
'help' => 'Powered by markdown',
|
||||
],
|
||||
'multiSelect' => [
|
||||
'selectText' => 'Press to select',
|
||||
'loadingText' => 'Loading...',
|
||||
|
|
|
@ -24,6 +24,11 @@ return [
|
|||
'pageInfo' => 'Page {currentPage} sur {pageCount}',
|
||||
'go_back' => 'Retour en arrière',
|
||||
'forms' => [
|
||||
'editor' => [
|
||||
'write' => 'Écrire',
|
||||
'preview' => 'Aperçu',
|
||||
'help' => 'Propulsé par markdown',
|
||||
],
|
||||
'multiSelect' => [
|
||||
'selectText' => 'Cliquez pour selectionner',
|
||||
'loadingText' => 'Chargement...',
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M8 11h4.5a2.5 2.5 0 1 0 0-5H8v5zm10 4.5a4.5 4.5 0 0 1-4.5 4.5H6V4h6.5a4.5 4.5 0 0 1 3.256 7.606A4.498 4.498 0 0 1 18 15.5zM8 13v5h5.5a2.5 2.5 0 1 0 0-5H8z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 306 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M17 11V4h2v17h-2v-8H7v8H5V4h2v7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 184 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M21 15v3h3v2h-3v3h-2v-3h-3v-2h3v-3h2zm.008-12c.548 0 .992.445.992.993v9.349A5.99 5.99 0 0 0 20 13V5H4l.001 14 9.292-9.293a.999.999 0 0 1 1.32-.084l.093.085 3.546 3.55a6.003 6.003 0 0 0-3.91 7.743L2.992 21A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016zM8 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 445 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M15 20H7v-2h2.927l2.116-12H9V4h8v2h-2.927l-2.116 12H15z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 207 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M8 4h13v2H8V4zM5 3v3h1v1H3V6h1V4H3V3h2zM3 14v-2.5h2V11H3v-1h3v2.5H4v.5h2v1H3zm2 5.5H3v-1h2V18H3v-1h3v4H3v-1h2v-.5zM8 11h13v2H8v-2zm0 7h13v2H8v-2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 297 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M8 4h13v2H8V4zM4.5 6.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 7a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 6.9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM8 11h13v2H8v-2zm0 7h13v2H8v-2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 326 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm4 12.5v-4l2 2 2-2v4h2v-7h-2l-2 2-2-2H5v7h2zm11-3v-4h-2v4h-2l3 3 3-3h-2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 295 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M4.583 17.321C3.553 16.227 3 15 3 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311 1.804.167 3.226 1.648 3.226 3.489a3.5 3.5 0 0 1-3.5 3.5c-1.073 0-2.099-.49-2.748-1.179zm10 0C13.553 16.227 13 15 13 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311 1.804.167 3.226 1.648 3.226 3.489a3.5 3.5 0 0 1-3.5 3.5c-1.073 0-2.099-.49-2.748-1.179z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 617 B |
|
@ -1,8 +1,10 @@
|
|||
import "@github/markdown-toolbar-element";
|
||||
import ClientTimezone from "./modules/ClientTimezone";
|
||||
import Clipboard from "./modules/Clipboard";
|
||||
import DateTimePicker from "./modules/DateTimePicker";
|
||||
import Dropdown from "./modules/Dropdown";
|
||||
import MarkdownEditor from "./modules/MarkdownEditor";
|
||||
import "./modules/markdown-preview";
|
||||
import "./modules/markdown-write-preview";
|
||||
import MultiSelect from "./modules/MultiSelect";
|
||||
import PublishMessageWarning from "./modules/PublishMessageWarning";
|
||||
import Select from "./modules/Select";
|
||||
|
@ -15,7 +17,6 @@ import Tooltip from "./modules/Tooltip";
|
|||
|
||||
Dropdown();
|
||||
Tooltip();
|
||||
MarkdownEditor();
|
||||
Select();
|
||||
MultiSelect();
|
||||
Slugify();
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
import { exampleSetup } from "prosemirror-example-setup";
|
||||
import "prosemirror-example-setup/style/style.css";
|
||||
import {
|
||||
defaultMarkdownParser,
|
||||
defaultMarkdownSerializer,
|
||||
schema,
|
||||
} from "prosemirror-markdown";
|
||||
import "prosemirror-menu/style/menu.css";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import "prosemirror-view/style/prosemirror.css";
|
||||
|
||||
class MarkdownView {
|
||||
textarea: HTMLTextAreaElement;
|
||||
|
||||
constructor(target: HTMLTextAreaElement) {
|
||||
this.textarea = target;
|
||||
this.textarea.classList.add("w-full", "h-full");
|
||||
}
|
||||
|
||||
content() {
|
||||
return this.textarea.innerHTML;
|
||||
}
|
||||
focus() {
|
||||
this.textarea.focus();
|
||||
}
|
||||
show() {
|
||||
this.textarea.classList.remove("hidden");
|
||||
}
|
||||
hide() {
|
||||
this.textarea.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
class ProseMirrorView {
|
||||
editorContainer: HTMLDivElement;
|
||||
view: EditorView;
|
||||
|
||||
constructor(target: HTMLTextAreaElement, content: string) {
|
||||
this.editorContainer = document.createElement("div");
|
||||
this.editorContainer.classList.add("bg-white", "border");
|
||||
this.editorContainer.style.minHeight = "200px";
|
||||
const editor = target.parentNode?.insertBefore(
|
||||
this.editorContainer,
|
||||
target.nextSibling
|
||||
);
|
||||
|
||||
this.view = new EditorView(editor, {
|
||||
state: EditorState.create({
|
||||
doc: defaultMarkdownParser.parse(content),
|
||||
plugins: exampleSetup({ schema }),
|
||||
}),
|
||||
dispatchTransaction: (transaction) => {
|
||||
const newState = this.view.state.apply(transaction);
|
||||
this.view.updateState(newState);
|
||||
|
||||
if (transaction.docChanged) {
|
||||
target.innerHTML = this.content();
|
||||
}
|
||||
},
|
||||
attributes: {
|
||||
class: "prose-sm px-3 py-2 overflow-y-auto focus:ring",
|
||||
style: "min-height: 200px; max-height: 500px",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
content(): string {
|
||||
return defaultMarkdownSerializer.serialize(this.view.state.doc) || "";
|
||||
}
|
||||
focus() {
|
||||
this.view.focus();
|
||||
}
|
||||
show() {
|
||||
this.editorContainer.classList.remove("hidden");
|
||||
}
|
||||
hide() {
|
||||
this.editorContainer.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
const MarkdownEditor = (): void => {
|
||||
const targets: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll(
|
||||
"textarea[data-editor='markdown']"
|
||||
);
|
||||
const activeClass = "font-semibold";
|
||||
|
||||
for (let i = 0; i < targets.length; i++) {
|
||||
const target = targets[i];
|
||||
|
||||
const wysiwygBtn = document.createElement("button");
|
||||
wysiwygBtn.classList.add(
|
||||
activeClass,
|
||||
"py-1",
|
||||
"px-2",
|
||||
"bg-white",
|
||||
"border",
|
||||
"text-xs",
|
||||
"outline-none",
|
||||
"focus:ring"
|
||||
);
|
||||
wysiwygBtn.setAttribute("type", "button");
|
||||
wysiwygBtn.innerHTML = "Wysiwyg";
|
||||
const markdownBtn = document.createElement("button");
|
||||
markdownBtn.classList.add(
|
||||
"py-1",
|
||||
"px-2",
|
||||
"bg-white",
|
||||
"border",
|
||||
"text-xs",
|
||||
"outline-none",
|
||||
"focus:ring"
|
||||
);
|
||||
markdownBtn.setAttribute("type", "button");
|
||||
markdownBtn.innerHTML = "Markdown";
|
||||
|
||||
const viewButtons = document.createElement("div");
|
||||
viewButtons.appendChild(wysiwygBtn);
|
||||
viewButtons.appendChild(markdownBtn);
|
||||
viewButtons.classList.add(
|
||||
"inline-flex",
|
||||
"absolute",
|
||||
"top-0",
|
||||
"right-0",
|
||||
"-mt-6"
|
||||
);
|
||||
|
||||
const markdownEditorContainer = document.createElement("div");
|
||||
markdownEditorContainer.classList.add("relative");
|
||||
markdownEditorContainer.style.minHeight = "200px";
|
||||
target.parentNode?.appendChild(markdownEditorContainer);
|
||||
markdownEditorContainer.appendChild(target);
|
||||
|
||||
// show WYSIWYG editor by default
|
||||
target.classList.add("hidden");
|
||||
const markdownView = new MarkdownView(target);
|
||||
const wysiwygView = new ProseMirrorView(target, markdownView.content());
|
||||
|
||||
markdownEditorContainer.appendChild(viewButtons);
|
||||
|
||||
markdownBtn.addEventListener("click", () => {
|
||||
if (markdownBtn.classList.contains(activeClass)) return;
|
||||
markdownBtn.classList.add(activeClass);
|
||||
wysiwygBtn.classList.remove(activeClass);
|
||||
wysiwygView.hide();
|
||||
markdownView.show();
|
||||
});
|
||||
|
||||
wysiwygBtn.addEventListener("click", () => {
|
||||
if (wysiwygBtn.classList.contains(activeClass)) return;
|
||||
wysiwygBtn.classList.add(activeClass);
|
||||
markdownBtn.classList.remove(activeClass);
|
||||
markdownView.hide();
|
||||
wysiwygView.show();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default MarkdownEditor;
|
|
@ -10,8 +10,11 @@ const MultiSelect = (): void => {
|
|||
|
||||
new Choices(multiSelect, {
|
||||
maxItemCount: parseInt(multiSelect.dataset.maxItemCount || "-1"),
|
||||
loadingText: multiSelect.dataset.loadingText,
|
||||
itemSelectText: multiSelect.dataset.selectText,
|
||||
maxItemText: multiSelect.dataset.maxItemText,
|
||||
noChoicesText: multiSelect.dataset.noChoicesText,
|
||||
noResultsText: multiSelect.dataset.noResultsText,
|
||||
removeItemButton: true,
|
||||
classNames: {
|
||||
containerOuter: "choices",
|
||||
|
|
|
@ -10,6 +10,11 @@ const Select = (): void => {
|
|||
const select = selects[i];
|
||||
|
||||
new Choices(select, {
|
||||
loadingText: select.dataset.loadingText,
|
||||
itemSelectText: select.dataset.selectText,
|
||||
maxItemText: select.dataset.maxItemText,
|
||||
noChoicesText: select.dataset.noChoicesText,
|
||||
noResultsText: select.dataset.noResultsText,
|
||||
classNames: {
|
||||
containerOuter: "choices",
|
||||
containerInner: "choices__inner",
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import MarkdownToolbarElement from "@github/markdown-toolbar-element";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import marked from "marked";
|
||||
|
||||
@customElement("markdown-preview")
|
||||
export class MarkdownPreview extends LitElement {
|
||||
@property()
|
||||
for!: string;
|
||||
|
||||
@property()
|
||||
_textarea!: HTMLTextAreaElement;
|
||||
|
||||
@property()
|
||||
_markdownToolbar!: MarkdownToolbarElement;
|
||||
|
||||
@property()
|
||||
_show = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this._textarea = document.getElementById(this.for) as HTMLTextAreaElement;
|
||||
this._markdownToolbar = document.querySelector(
|
||||
`markdown-toolbar[for=${this.for}]`
|
||||
) as MarkdownToolbarElement;
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this._show = false;
|
||||
this.classList.add("hidden");
|
||||
this._markdownToolbar.classList.remove("hidden");
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this._show = true;
|
||||
this.classList.remove("hidden");
|
||||
this._markdownToolbar.classList.add("hidden");
|
||||
}
|
||||
|
||||
markdownToHtml(): string {
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.link = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const link = marked.Renderer.prototype.link.apply(this, arguments as any);
|
||||
return link.replace("<a", "<a target='_blank' rel='noopener noreferrer'");
|
||||
};
|
||||
|
||||
return marked(this._textarea.value, {
|
||||
renderer: renderer,
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult<1> {
|
||||
return html`${this._show
|
||||
? html`${unsafeHTML(this.markdownToHtml())}`
|
||||
: html``}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
|
||||
import { MarkdownPreview } from "./markdown-preview";
|
||||
|
||||
@customElement("markdown-write-preview")
|
||||
export class MarkdownWritePreview extends LitElement {
|
||||
@property()
|
||||
for!: string;
|
||||
|
||||
@property()
|
||||
_textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
@property()
|
||||
_markdownPreview!: MarkdownPreview;
|
||||
|
||||
@queryAssignedNodes("write", true)
|
||||
_write!: NodeListOf<HTMLButtonElement>;
|
||||
|
||||
@queryAssignedNodes("preview", true)
|
||||
_preview!: NodeListOf<HTMLButtonElement>;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this._textarea = document.getElementById(this.for) as HTMLTextAreaElement;
|
||||
this._markdownPreview = document.querySelector(
|
||||
`markdown-preview[for=${this.for}]`
|
||||
) as MarkdownPreview;
|
||||
}
|
||||
|
||||
write(): void {
|
||||
this._markdownPreview.hide();
|
||||
this._write[0].classList.add("font-semibold");
|
||||
this._preview[0].classList.remove("font-semibold");
|
||||
}
|
||||
|
||||
preview(): void {
|
||||
this._markdownPreview.show();
|
||||
this._preview[0].classList.add("font-semibold");
|
||||
this._write[0].classList.remove("font-semibold");
|
||||
}
|
||||
|
||||
render(): TemplateResult<1> {
|
||||
return html`<slot name="write" @click="${this.write}"></slot>
|
||||
<slot name="preview" @click="${this.preview}"></slot>`;
|
||||
}
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
export {};
|
||||
import "@github/markdown-toolbar-element";
|
||||
import "./modules/markdown-preview";
|
||||
import "./modules/markdown-write-preview";
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import "prosemirror-example-setup/style/style.css";
|
||||
import "prosemirror-menu/style/menu.css";
|
||||
import "prosemirror-view/style/prosemirror.css";
|
||||
declare const MarkdownEditor: () => void;
|
||||
export default MarkdownEditor;
|
|
@ -0,0 +1,13 @@
|
|||
import MarkdownToolbarElement from "@github/markdown-toolbar-element";
|
||||
import { LitElement, TemplateResult } from "lit";
|
||||
export declare class MarkdownPreview extends LitElement {
|
||||
for: string;
|
||||
_textarea: HTMLTextAreaElement;
|
||||
_markdownToolbar: MarkdownToolbarElement;
|
||||
_show: boolean;
|
||||
connectedCallback(): void;
|
||||
hide(): void;
|
||||
show(): void;
|
||||
markdownToHtml(): string;
|
||||
render(): TemplateResult<1>;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { LitElement, TemplateResult } from "lit";
|
||||
import { MarkdownPreview } from "./markdown-preview";
|
||||
export declare class MarkdownWritePreview extends LitElement {
|
||||
for: string;
|
||||
_textarea: HTMLTextAreaElement | null;
|
||||
_markdownPreview: MarkdownPreview;
|
||||
_write: NodeListOf<HTMLButtonElement>;
|
||||
_preview: NodeListOf<HTMLButtonElement>;
|
||||
connectedCallback(): void;
|
||||
write(): void;
|
||||
preview(): void;
|
||||
render(): TemplateResult<1>;
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
<?= $this->include('admin/_sidebar') ?>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
<main class="overflow-hidden holy-grail-main">
|
||||
<main class="holy-grail-main">
|
||||
<header class="text-white bg-pine-900">
|
||||
<div class="container flex flex-wrap items-end justify-between px-2 py-10 mx-auto md:px-12 gap-y-6 gap-x-6">
|
||||
<div class="flex flex-col">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_label(lang('Contributor.form.user'), 'user') ?>
|
||||
<?= form_dropdown('user', $userOptions, old('user', ''), [
|
||||
<?= form_dropdown('user', $userOptions, [old('user', '')], [
|
||||
'id' => 'user',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
|
@ -25,7 +25,7 @@
|
|||
]) ?>
|
||||
|
||||
<?= form_label(lang('Contributor.form.role'), 'role') ?>
|
||||
<?= form_dropdown('role', $roleOptions, old('role', ''), [
|
||||
<?= form_dropdown('role', $roleOptions, [old('role', '')], [
|
||||
'id' => 'role',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_label(lang('Contributor.form.role'), 'role') ?>
|
||||
<?= form_dropdown('role', $roleOptions, old('role', $contributorGroupId), [
|
||||
<?= form_dropdown('role', $roleOptions, [old('role', $contributorGroupId)], [
|
||||
'id' => 'role',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
|
|
|
@ -201,15 +201,13 @@
|
|||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Episode.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'description',
|
||||
'name' => 'description',
|
||||
'class' => 'form-textarea',
|
||||
'required' => 'required',
|
||||
],
|
||||
old('description', '', false),
|
||||
'data-editor="markdown"',
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
@ -219,19 +217,18 @@
|
|||
'description_footer',
|
||||
[],
|
||||
lang('Episode.form.description_footer_hint'),
|
||||
true
|
||||
) ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'description_footer',
|
||||
'name' => 'description_footer',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old(
|
||||
'description_footer',
|
||||
$podcast->episode_description_footer_markdown ?? '',
|
||||
false,
|
||||
),
|
||||
'data-editor="markdown"',
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -211,15 +211,13 @@
|
|||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Episode.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'description',
|
||||
'name' => 'description',
|
||||
'class' => 'form-textarea',
|
||||
'required' => 'required',
|
||||
],
|
||||
old('description', $episode->description_markdown, false),
|
||||
'data-editor="markdown"',
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
@ -229,19 +227,18 @@
|
|||
'description_footer',
|
||||
[],
|
||||
lang('Episode.form.description_footer_hint'),
|
||||
true
|
||||
) ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'description_footer',
|
||||
'name' => 'description_footer',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old(
|
||||
'description_footer',
|
||||
$podcast->episode_description_footer_markdown ?? '',
|
||||
false,
|
||||
),
|
||||
'data-editor="markdown"',
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -38,15 +38,14 @@
|
|||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Page.form.content'), 'content') ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'content',
|
||||
'name' => 'content',
|
||||
'class' => 'form-textarea',
|
||||
'required' => 'required',
|
||||
],
|
||||
old('content', '', false),
|
||||
'data-editor="markdown"',
|
||||
['rows' => '20']
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -38,15 +38,13 @@
|
|||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Page.form.content'), 'content') ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'content',
|
||||
'name' => 'content',
|
||||
'class' => 'form-textarea',
|
||||
'required' => 'required',
|
||||
],
|
||||
old('content', $page->content_markdown, false),
|
||||
'data-editor="markdown"',
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -82,15 +82,13 @@
|
|||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Podcast.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
<?= form_markdown_editor(
|
||||
[
|
||||
'id' => 'description',
|
||||
'name' => 'description',
|
||||
'class' => 'form-textarea',
|
||||
'required' => 'required',
|
||||
],
|
||||
old('description', '', false),
|
||||
'data-editor="markdown"',
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
@ -103,14 +101,14 @@
|
|||
) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.language'), 'language') ?>
|
||||
<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [
|
||||
<?= form_dropdown('language', $languageOptions, [old('language', $browserLang)], [
|
||||
'id' => 'language',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.category'), 'category') ?>
|
||||
<?= form_dropdown('category', $categoryOptions, old('category', ''), [
|
||||
<?= form_dropdown('category', $categoryOptions, [old('category', '')], [
|
||||
'id' => 'category',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
|
@ -127,7 +125,7 @@
|
|||
<?= form_multiselect(
|
||||
'other_categories[]',
|
||||
$categoryOptions,
|
||||
old('other_categories', []),
|
||||
[old('other_categories', '')],
|
||||
[
|
||||
'id' => 'other_categories',
|
||||
'class' => 'mb-4',
|
||||
|
@ -282,11 +280,11 @@
|
|||
|
||||
<?= form_label(lang('Podcast.form.partnership')) ?>
|
||||
<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
|
||||
<div class="flex flex-col flex-shrink w-32">
|
||||
<div class="flex flex-col flex-shrink">
|
||||
<?= form_label(
|
||||
lang('Podcast.form.partner_id'),
|
||||
'partner_id',
|
||||
[],
|
||||
['class' => 'text-sm'],
|
||||
lang('Podcast.form.partner_id_hint'),
|
||||
true,
|
||||
) ?>
|
||||
|
@ -297,11 +295,11 @@
|
|||
'value' => old('partner_id'),
|
||||
]) ?>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col">
|
||||
<?= form_label(
|
||||
lang('Podcast.form.partner_link_url'),
|
||||
'partner_link_url',
|
||||
[],
|
||||
['class' => 'text-sm'],
|
||||
lang('Podcast.form.partner_link_url_hint'),
|
||||
true,
|
||||
) ?>
|
||||
|
@ -312,11 +310,11 @@
|
|||
'value' => old('partner_link_url'),
|
||||
]) ?>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col">
|
||||
<?= form_label(
|
||||
lang('Podcast.form.partner_image_url'),
|
||||
'partner_image_url',
|
||||
[],
|
||||
['class' => 'text-sm'],
|
||||
lang('Podcast.form.partner_image_url_hint'),
|
||||
true,
|
||||
) ?>
|
||||
|
|
|
@ -68,15 +68,12 @@
|
|||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Podcast.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
<?= form_markdown_editor([
|
||||
'id' => 'description',
|
||||
'name' => 'description',
|
||||
'class' => 'form-textarea',
|
||||
'required' => 'required',
|
||||
],
|
||||
old('description', $podcast->description_markdown, false),
|
||||
'data-editor="markdown"',
|
||||
old('description', $podcast->description_markdown, false)
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
|
@ -92,7 +89,7 @@
|
|||
<?= form_dropdown(
|
||||
'language',
|
||||
$languageOptions,
|
||||
old('language', $podcast->language_code),
|
||||
[old('language', $podcast->language_code)],
|
||||
[
|
||||
'id' => 'language',
|
||||
'class' => 'form-select mb-4',
|
||||
|
@ -104,7 +101,7 @@
|
|||
<?= form_dropdown(
|
||||
'category',
|
||||
$categoryOptions,
|
||||
old('category', (string) $podcast->category_id),
|
||||
[old('category', (string) $podcast->category_id)],
|
||||
[
|
||||
'id' => 'category',
|
||||
'class' => 'form-select mb-4',
|
||||
|
|
|
@ -68,14 +68,14 @@
|
|||
</div>
|
||||
|
||||
<?= form_label(lang('Podcast.form.language'), 'language') ?>
|
||||
<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [
|
||||
<?= form_dropdown('language', $languageOptions, [old('language', $browserLang)], [
|
||||
'id' => 'language',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.category'), 'category') ?>
|
||||
<?= form_dropdown('category', $categoryOptions, old('category', ''), [
|
||||
<?= form_dropdown('category', $categoryOptions, [old('category', '')], [
|
||||
'id' => 'category',
|
||||
'class' => 'form-select mb-4',
|
||||
'required' => 'required',
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
'redis' => lang('Install.form.cacheHandlerOptions.redis'),
|
||||
'predis' => lang('Install.form.cacheHandlerOptions.predis'),
|
||||
],
|
||||
old('cache_handler', 'file'),
|
||||
[old('cache_handler', 'file')],
|
||||
[
|
||||
'id' => 'cache_handler',
|
||||
'name' => 'cache_handler',
|
||||
|
|
25
package.json
|
@ -28,19 +28,16 @@
|
|||
"prepare": "is-ci || husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amcharts/amcharts4": "^4.10.20",
|
||||
"@amcharts/amcharts4-geodata": "^4.1.21",
|
||||
"@amcharts/amcharts4": "^4.10.20",
|
||||
"@github/markdown-toolbar-element": "^1.5.1",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||
"choices.js": "^9.0.1",
|
||||
"flatpickr": "^4.6.9",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet.markercluster": "^1.5.1",
|
||||
"leaflet": "^1.7.1",
|
||||
"lit": "^2.0.0-rc.2",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.5.1",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-view": "^1.18.11"
|
||||
"marked": "^2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^13.1.0",
|
||||
|
@ -53,31 +50,33 @@
|
|||
"@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.4",
|
||||
"@typescript-eslint/parser": "^4.28.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.5",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"cpy-cli": "^3.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cssnano": "^5.0.7",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint": "^7.31.0",
|
||||
"husky": "^7.0.1",
|
||||
"is-ci": "^3.0.0",
|
||||
"lint-staged": "^11.1.1",
|
||||
"lit": "^2.0.0-rc.2",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "2.3.2",
|
||||
"prettier-plugin-organize-imports": "^2.3.3",
|
||||
"prettier": "2.3.2",
|
||||
"semantic-release": "^17.4.4",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint": "^13.13.1",
|
||||
"svgo": "^2.3.1",
|
||||
"tailwindcss": "^2.2.7",
|
||||
"typescript": "^4.3.5",
|
||||
"vite": "^2.4.3"
|
||||
"vite": "^2.4.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,css,md,json}": "prettier --write",
|
||||
|
|