feat: refactorer l'interface d'édition de bookmarks avec UI sombre raffinée, éditeur Markdown Toast UI intégré, système de tags interactif avec pills et suppression, grille de toggles pour read later/note/private, variables CSS personnalisées pour thème cohérent, et amélioration responsive avec grid layouts optimisés et support mobile complet
This commit is contained in:
parent
6f58f6cd67
commit
5b6b0fd163
@ -3840,4 +3840,353 @@ select:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Add/Edit Bookmark - Refined Dark UI ===== */
|
||||
.page-edit {
|
||||
--bookmark-panel-bg: #121c30;
|
||||
--bookmark-panel-border: #25324a;
|
||||
--bookmark-panel-soft: #1a2640;
|
||||
--bookmark-input-bg: #0c1528;
|
||||
--bookmark-input-border: #334769;
|
||||
--bookmark-input-focus: #70a0ff;
|
||||
--bookmark-input-focus-ring: rgba(112, 160, 255, 0.25);
|
||||
--bookmark-text-main: #eaf1ff;
|
||||
--bookmark-text-muted: #9fb1d4;
|
||||
--bookmark-tag-bg: #2f2f90;
|
||||
--bookmark-tag-bg-hover: #3939a6;
|
||||
--bookmark-tag-text: #cfdaff;
|
||||
--bookmark-tag-remove-bg: rgba(255, 255, 255, 0.16);
|
||||
--bookmark-tag-remove-bg-hover: rgba(255, 255, 255, 0.26);
|
||||
--bookmark-save-bg: linear-gradient(135deg, #6fa8ff 0%, #4d82f0 100%);
|
||||
}
|
||||
|
||||
.page-edit .editlinkform-col {
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--bookmark-panel-border);
|
||||
background: linear-gradient(180deg, #1a2943 0%, var(--bookmark-panel-bg) 100%);
|
||||
box-shadow: 0 16px 42px rgba(0, 8, 22, 0.42);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-header {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
border-bottom: 1px solid var(--bookmark-panel-border);
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.bookmark-editor-title {
|
||||
margin: 0;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.2;
|
||||
color: var(--bookmark-text-main);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.bookmark-editor-meta {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--bookmark-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-card .card-body {
|
||||
padding: 1.5rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-field-group {
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-field-group .form-label {
|
||||
margin-bottom: 0.45rem;
|
||||
color: var(--bookmark-text-main);
|
||||
font-weight: 600;
|
||||
font-size: 0.86rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-card .form-control,
|
||||
.page-edit .bookmark-editor-card input[type="text"],
|
||||
.page-edit .bookmark-editor-card textarea {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--bookmark-input-border);
|
||||
background: var(--bookmark-input-bg);
|
||||
color: var(--bookmark-text-main);
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-card .form-control::placeholder,
|
||||
.page-edit .bookmark-tags-text-input::placeholder {
|
||||
color: var(--bookmark-text-muted);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-card .form-control:focus,
|
||||
.page-edit .bookmark-editor-card input[type="text"]:focus,
|
||||
.page-edit .bookmark-editor-card textarea:focus,
|
||||
.page-edit .bookmark-tags-text-input:focus {
|
||||
border-color: var(--bookmark-input-focus);
|
||||
box-shadow: 0 0 0 3px var(--bookmark-input-focus-ring);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-form.is-enhanced .bookmark-editor-source {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-markdown-editor {
|
||||
border: 1px solid var(--bookmark-input-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--bookmark-input-bg);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-sublabel {
|
||||
margin-top: 0.5rem;
|
||||
color: var(--bookmark-text-muted);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-sublabel a {
|
||||
color: #8fb8ff;
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-defaultUI,
|
||||
.page-edit .toastui-editor-md-container,
|
||||
.page-edit .toastui-editor-ww-container,
|
||||
.page-edit .toastui-editor-toolbar,
|
||||
.page-edit .toastui-editor-mode-switch {
|
||||
background: var(--bookmark-input-bg);
|
||||
}
|
||||
.page-edit .toastui-editor-defaultUI {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-toolbar {
|
||||
border-bottom: 1px solid var(--bookmark-input-border);
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-toolbar button {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-toolbar button:hover,
|
||||
.page-edit .toastui-editor-toolbar button.active,
|
||||
.page-edit .toastui-editor-toolbar button:focus-visible {
|
||||
background-color: rgba(126, 168, 255, 0.16);
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-toolbar-icons {
|
||||
opacity: 0.96;
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-toolbar-divider {
|
||||
background-color: var(--bookmark-input-border);
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-mode-switch .tab-item {
|
||||
color: var(--bookmark-text-main);
|
||||
}
|
||||
|
||||
.page-edit .toastui-editor-md-tab-container,
|
||||
.page-edit .toastui-editor-mode-switch {
|
||||
border-top: 1px solid var(--bookmark-input-border);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tags-input {
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--bookmark-input-border);
|
||||
background: var(--bookmark-input-bg);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tags-input:focus-within {
|
||||
border-color: var(--bookmark-input-focus);
|
||||
box-shadow: 0 0 0 3px var(--bookmark-input-focus-ring);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tag-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
height: 34px;
|
||||
padding: 0 0.4rem 0 0.8rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(191, 203, 255, 0.12);
|
||||
background: var(--bookmark-tag-bg);
|
||||
color: var(--bookmark-tag-text);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tag-pill:hover {
|
||||
transform: translateY(-1px);
|
||||
background: var(--bookmark-tag-bg-hover);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tag-remove {
|
||||
border: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
background: var(--bookmark-tag-remove-bg);
|
||||
color: var(--bookmark-tag-text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tag-remove:hover,
|
||||
.page-edit .bookmark-tag-remove:focus-visible {
|
||||
background: var(--bookmark-tag-remove-bg-hover);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tag-remove:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tags-text-input {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--bookmark-text-main);
|
||||
min-width: 190px;
|
||||
flex: 1;
|
||||
font-size: 0.92rem;
|
||||
padding: 0.45rem 0.4rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tags-text-input:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-toggle-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
color: var(--bookmark-text-main);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-toggle-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #7ea8ff;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer {
|
||||
background: var(--bookmark-panel-soft);
|
||||
border-top: 1px solid var(--bookmark-panel-border);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn {
|
||||
min-height: 42px;
|
||||
border-radius: 10px;
|
||||
padding-inline: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn-primary {
|
||||
background: var(--bookmark-save-bg);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 10px 22px rgba(77, 130, 240, 0.35);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn-primary:hover {
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn-danger {
|
||||
background: #c84e4e;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn-danger:hover {
|
||||
background: #dd5a5a;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.page-edit .editlinkform-col {
|
||||
max-width: 100%;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-header,
|
||||
.page-edit .bookmark-editor-card .card-body,
|
||||
.page-edit .bookmark-editor-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-toggle-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tags-input {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-tag-pill {
|
||||
height: 32px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer {
|
||||
flex-wrap: wrap;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn,
|
||||
.page-edit .bookmark-editor-footer a.btn {
|
||||
flex: 1 1 calc(50% - 0.4rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page-edit .bookmark-tags-text-input {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.page-edit .bookmark-editor-footer .btn,
|
||||
.page-edit .bookmark-editor-footer a.btn {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
@ -15,10 +15,19 @@
|
||||
{if="!isset($index)"}
|
||||
{$index=""}
|
||||
{/if}
|
||||
{$batchModeValue=empty($batch_mode) ? '0' : '1'}
|
||||
{$readLaterChecked=strpos(' ' . $link.tags . ' ', ' readlater ') != false || strpos(' ' . $link.tags . ' ', ' toread ') != false}
|
||||
{$noteDefaultChecked=$link_is_new && empty($link.url)}
|
||||
{$noteChecked=strpos(' ' . $link.tags . ' ', ' note ') != false || $noteDefaultChecked}
|
||||
{$effectiveTags=$link.tags}
|
||||
{if="$noteDefaultChecked && strpos(' ' . $effectiveTags . ' ', ' note ') == false"}
|
||||
{$effectiveTags=trim($effectiveTags . ' note')}
|
||||
{/if}
|
||||
{$privateChecked=$link.private == true || $link_is_new}
|
||||
<div id="editlinkform{$index}" class="editlinkform container page-edit">
|
||||
<div class="row editlinkform-row">
|
||||
<div class="col-md-6 col-md-offset-3 editlinkform-col">
|
||||
<form method="post" name="linkform" class="card" action="{$base_path}/admin/shaare">
|
||||
<form method="post" name="linkform" class="card bookmark-editor-card bookmark-editor-form" action="{$base_path}/admin/shaare" data-batch-mode="{$batchModeValue}">
|
||||
{if="isset($link.id)"}
|
||||
<input type="hidden" name="lf_id" value="{$link.id}">
|
||||
{/if}
|
||||
@ -28,54 +37,74 @@
|
||||
<input type="hidden" name="returnurl" value="{$http_referer}">
|
||||
{/if}
|
||||
|
||||
<div class="card-header">
|
||||
{if="!$link_is_new"}Edit Bookmark{else}New Bookmark{/if}
|
||||
{if="!$link_is_new"}<span class="card-subheader" style="font-size:0.85rem; color:var(--text-muted); font-weight:normal;"> - {'created on'|t} {$link.created|format_date}</span>{/if}
|
||||
<div class="card-header bookmark-editor-header">
|
||||
<div class="bookmark-editor-title-wrap">
|
||||
<h1 class="bookmark-editor-title">{if="!$link_is_new"}Edit Bookmark{else}New Bookmark{/if}</h1>
|
||||
{if="!$link_is_new"}
|
||||
<p class="bookmark-editor-meta">{'created on'|t} {$link.created|format_date}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="form-group bookmark-field-group">
|
||||
<label class="form-label" for="lf_url{$index}">{'URL'|t}</label>
|
||||
<input type="text" class="form-control" name="lf_url" id="lf_url{$index}" value="{$link.url}" placeholder="https://...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group bookmark-field-group">
|
||||
<label class="form-label" for="lf_title{$index}">{'Title'|t}</label>
|
||||
<div class="{$asyncLoadClass}">
|
||||
<input type="text" class="form-control{if="empty($batch_mode) && $link.title==''"} autofocus{/if}" name="lf_title" id="lf_title{$index}" value="{$link.title}" placeholder="Page title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="lf_description{$index}">{'Description'|t}</label>
|
||||
<div class="form-group bookmark-field-group">
|
||||
<label class="form-label" for="lf_description{$index}">{'Description'|t} (Markdown)</label>
|
||||
<div class="{$asyncLoadClass}">
|
||||
<textarea class="form-control{if="empty($batch_mode) && $link.description==''"} autofocus{/if}" name="lf_description" id="lf_description{$index}" placeholder="Add a description..." rows="5">{$link.description}</textarea>
|
||||
<textarea class="form-control bookmark-editor-source{if="empty($batch_mode) && $link.description==''"} autofocus{/if}" name="lf_description" id="lf_description{$index}" placeholder="Add a description..." rows="6">{$link.description}</textarea>
|
||||
<div id="lf_description_editor{$index}" class="bookmark-markdown-editor" aria-label="Markdown editor"></div>
|
||||
</div>
|
||||
{if="$formatter==='markdown'"}
|
||||
<div class="sublabel">
|
||||
<div class="sublabel bookmark-editor-sublabel">
|
||||
{'Description will be rendered with'|t} <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank" rel="noopener">{'Markdown syntax'|t}</a>.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group bookmark-field-group">
|
||||
<label class="form-label" for="lf_tags{$index}">{'Tags'|t}</label>
|
||||
<div class="{$asyncLoadClass}">
|
||||
<input type="text" class="form-control" id="lf_tags{$index}" name="lf_tags" {if="empty($batch_mode)"}class="autofocus form-control"{/if} value="{$link.tags}"
|
||||
data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" placeholder="tags separated by space" />
|
||||
<input type="text" class="bookmark-tags-hidden-input" id="lf_tags{$index}" name="lf_tags" value="{$effectiveTags}" data-tag-options="{loop="$tags"}{$key},{/loop}" autocomplete="off" />
|
||||
<div class="bookmark-tags-input" data-tags-container="lf_tags{$index}">
|
||||
<div class="bookmark-tags-list" id="bookmark-tags-list{$index}"></div>
|
||||
<input type="text" class="bookmark-tags-text-input" id="bookmark-tags-text{$index}" placeholder="Type a tag and press Enter or Space" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{if="isset($edit_link_plugin)"}
|
||||
{loop="$edit_link_plugin"}
|
||||
{if="!strstr(strtolower($value), 'lf_readlater') && !strstr(strtolower($value), 'lf_private')"}
|
||||
<div class="form-group">
|
||||
{$value}
|
||||
</div>
|
||||
{/if}
|
||||
{/loop}
|
||||
{/if}
|
||||
|
||||
<div class="form-group checkbox-wrapper">
|
||||
<input type="checkbox" {if="$link.private === true"}checked="checked"{/if} name="lf_private" id="lf_private{$index}" style="width:auto;"/>
|
||||
<label for="lf_private{$index}" style="margin:0;">{'Private bookmark'|t}</label>
|
||||
<div class="bookmark-toggle-grid">
|
||||
<label class="bookmark-toggle-item" for="lf_readlater{$index}">
|
||||
<input type="checkbox" id="lf_readlater{$index}" class="bookmark-toggle-readlater" {if="$readLaterChecked"}checked="checked"{/if}>
|
||||
<span>{'Read it later'|t}</span>
|
||||
</label>
|
||||
<label class="bookmark-toggle-item" for="lf_note{$index}">
|
||||
<input type="checkbox" id="lf_note{$index}" class="bookmark-toggle-note" {if="$noteChecked"}checked="checked"{/if}>
|
||||
<span>Note</span>
|
||||
</label>
|
||||
<label class="bookmark-toggle-item" for="lf_private{$index}">
|
||||
<input type="checkbox" {if="$privateChecked"}checked="checked"{/if} name="lf_private" id="lf_private{$index}" />
|
||||
<span>{'Private'|t}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="card-footer bookmark-editor-footer">
|
||||
{if="!empty($batch_mode)"}
|
||||
<button type="button" name="cancel-batch-link" class="btn btn-secondary">{'Cancel'|t}</button>
|
||||
{/if}
|
||||
|
||||
@ -30,6 +30,12 @@
|
||||
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/awesomplete.min.js?v=1.0.4" defer></script>
|
||||
{/if}
|
||||
|
||||
{if="$pageName=='editlink' || $pageName=='editlinkbatch'"}
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/theme/toastui-editor-dark.min.css" />
|
||||
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js" defer></script>
|
||||
{/if}
|
||||
|
||||
<!-- Preconnect to external domains for performance -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
@ -338,7 +338,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const isSearchOpen = searchOverlay?.classList.contains('show');
|
||||
const isTyping = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA';
|
||||
const target = e.target;
|
||||
const isTyping = Boolean(
|
||||
target && (
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.tagName === 'SELECT' ||
|
||||
target.isContentEditable ||
|
||||
(typeof target.closest === 'function' && target.closest('.toastui-editor-defaultUI, .toastui-editor-main, .toastui-editor-contents, .CodeMirror'))
|
||||
)
|
||||
);
|
||||
|
||||
// Handle ESC - always close search/filter
|
||||
if (e.key === 'Escape') {
|
||||
@ -373,7 +382,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// S to open search (when not typing)
|
||||
if (!isTyping && (e.key === 's' || e.key === 'S')) {
|
||||
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 's' || e.key === 'S')) {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
@ -1145,6 +1154,245 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== Bookmark Editor Form (Markdown + Tags UI) =====
|
||||
function initBookmarkEditorForms() {
|
||||
const forms = document.querySelectorAll('.bookmark-editor-form[data-batch-mode="0"]');
|
||||
if (!forms.length) return;
|
||||
|
||||
const normalizeTag = (value) => value.trim().replace(/\s+/g, '-');
|
||||
|
||||
forms.forEach((form) => {
|
||||
form.classList.add('is-enhanced');
|
||||
|
||||
const descriptionSource = form.querySelector('.bookmark-editor-source');
|
||||
const editorMount = form.querySelector('.bookmark-markdown-editor');
|
||||
const hiddenTagsInput = form.querySelector('.bookmark-tags-hidden-input');
|
||||
const tagsInputContainer = form.querySelector('.bookmark-tags-input');
|
||||
const tagsList = form.querySelector('.bookmark-tags-list');
|
||||
const tagsTextInput = form.querySelector('.bookmark-tags-text-input');
|
||||
const readLaterCheckbox = form.querySelector('.bookmark-toggle-readlater');
|
||||
const noteCheckbox = form.querySelector('.bookmark-toggle-note');
|
||||
const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
|
||||
// Markdown editor
|
||||
if (descriptionSource && editorMount && window.toastui && window.toastui.Editor) {
|
||||
const previewStyle = window.innerWidth < 992 ? 'tab' : 'vertical';
|
||||
|
||||
const markdownEditor = new window.toastui.Editor({
|
||||
el: editorMount,
|
||||
height: '320px',
|
||||
initialEditType: 'markdown',
|
||||
previewStyle,
|
||||
initialValue: descriptionSource.value || '',
|
||||
placeholder: descriptionSource.getAttribute('placeholder') || '',
|
||||
theme: isDarkTheme ? 'dark' : undefined,
|
||||
usageStatistics: false,
|
||||
});
|
||||
|
||||
form.addEventListener('submit', () => {
|
||||
descriptionSource.value = markdownEditor.getMarkdown();
|
||||
});
|
||||
} else if (editorMount) {
|
||||
editorMount.style.display = 'none';
|
||||
if (descriptionSource) {
|
||||
descriptionSource.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Pill tags
|
||||
if (!hiddenTagsInput || !tagsList || !tagsTextInput) return;
|
||||
|
||||
const availableTags = (hiddenTagsInput.getAttribute('data-tag-options') || '')
|
||||
.split(',')
|
||||
.map((tag) => normalizeTag(tag))
|
||||
.filter(Boolean);
|
||||
|
||||
const getSuggestionList = () => availableTags
|
||||
.filter((tag) => !tags.some((existing) => existing.toLowerCase() === tag.toLowerCase()));
|
||||
|
||||
let tagsAutocomplete = null;
|
||||
|
||||
const refreshAutocomplete = () => {
|
||||
if (!tagsAutocomplete) return;
|
||||
tagsAutocomplete.list = getSuggestionList();
|
||||
};
|
||||
|
||||
if (tagsInputContainer) {
|
||||
tagsInputContainer.addEventListener('click', () => tagsTextInput.focus());
|
||||
}
|
||||
|
||||
let tags = hiddenTagsInput.value
|
||||
.split(/[\s,]+/)
|
||||
.map((tag) => normalizeTag(tag))
|
||||
.filter(Boolean);
|
||||
|
||||
const updateHiddenTags = () => {
|
||||
hiddenTagsInput.value = tags.join(' ');
|
||||
};
|
||||
|
||||
const syncReadLaterCheckbox = () => {
|
||||
if (!readLaterCheckbox) return;
|
||||
readLaterCheckbox.checked = tags.some((tag) => /^(readlater|toread)$/i.test(tag));
|
||||
};
|
||||
|
||||
const syncNoteCheckbox = () => {
|
||||
if (!noteCheckbox) return;
|
||||
noteCheckbox.checked = tags.some((tag) => /^note$/i.test(tag));
|
||||
};
|
||||
|
||||
const addTag = (rawTag) => {
|
||||
const tag = normalizeTag(rawTag);
|
||||
if (!tag) return;
|
||||
|
||||
const exists = tags.some((existing) => existing.toLowerCase() === tag.toLowerCase());
|
||||
if (!exists) {
|
||||
tags.push(tag);
|
||||
updateHiddenTags();
|
||||
syncReadLaterCheckbox();
|
||||
syncNoteCheckbox();
|
||||
renderTags();
|
||||
refreshAutocomplete();
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagToRemove) => {
|
||||
tags = tags.filter((tag) => tag.toLowerCase() !== tagToRemove.toLowerCase());
|
||||
updateHiddenTags();
|
||||
syncReadLaterCheckbox();
|
||||
syncNoteCheckbox();
|
||||
renderTags();
|
||||
refreshAutocomplete();
|
||||
};
|
||||
|
||||
const commitInputValue = () => {
|
||||
const value = tagsTextInput.value;
|
||||
if (!value.trim()) return;
|
||||
|
||||
value
|
||||
.split(/[\s,]+/)
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.forEach(addTag);
|
||||
|
||||
tagsTextInput.value = '';
|
||||
refreshAutocomplete();
|
||||
};
|
||||
|
||||
function renderTags() {
|
||||
tagsList.innerHTML = '';
|
||||
|
||||
tags.forEach((tag) => {
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'bookmark-tag-pill';
|
||||
pill.setAttribute('data-tag', tag);
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'bookmark-tag-label';
|
||||
label.textContent = tag;
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'bookmark-tag-remove';
|
||||
removeBtn.setAttribute('aria-label', `Remove tag ${tag}`);
|
||||
removeBtn.innerHTML = '<i class="mdi mdi-close" aria-hidden="true"></i>';
|
||||
removeBtn.addEventListener('click', () => removeTag(tag));
|
||||
|
||||
pill.appendChild(label);
|
||||
pill.appendChild(removeBtn);
|
||||
tagsList.appendChild(pill);
|
||||
});
|
||||
}
|
||||
|
||||
tagsTextInput.addEventListener('keydown', (event) => {
|
||||
const hasOpenAutocomplete = Boolean(tagsAutocomplete && tagsAutocomplete.ul && !tagsAutocomplete.ul.hasAttribute('hidden'));
|
||||
const shouldCommit = event.key === ' ' || event.key === ',' || (event.key === 'Enter' && !hasOpenAutocomplete);
|
||||
|
||||
if (shouldCommit) {
|
||||
event.preventDefault();
|
||||
commitInputValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Backspace' && !tagsTextInput.value && tags.length) {
|
||||
removeTag(tags[tags.length - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
tagsTextInput.addEventListener('blur', commitInputValue);
|
||||
|
||||
tagsTextInput.addEventListener('paste', (event) => {
|
||||
const pasted = event.clipboardData ? event.clipboardData.getData('text') : '';
|
||||
if (!pasted) return;
|
||||
|
||||
event.preventDefault();
|
||||
pasted
|
||||
.split(/[\s,]+/)
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.forEach(addTag);
|
||||
tagsTextInput.value = '';
|
||||
});
|
||||
|
||||
if (readLaterCheckbox) {
|
||||
readLaterCheckbox.addEventListener('change', () => {
|
||||
tags = tags.filter((tag) => !/^(readlater|toread)$/i.test(tag));
|
||||
|
||||
if (readLaterCheckbox.checked) {
|
||||
tags.push('readlater');
|
||||
}
|
||||
|
||||
updateHiddenTags();
|
||||
syncNoteCheckbox();
|
||||
renderTags();
|
||||
refreshAutocomplete();
|
||||
});
|
||||
}
|
||||
|
||||
if (noteCheckbox) {
|
||||
noteCheckbox.addEventListener('change', () => {
|
||||
tags = tags.filter((tag) => !/^note$/i.test(tag));
|
||||
|
||||
if (noteCheckbox.checked) {
|
||||
tags.push('note');
|
||||
}
|
||||
|
||||
updateHiddenTags();
|
||||
syncReadLaterCheckbox();
|
||||
renderTags();
|
||||
refreshAutocomplete();
|
||||
});
|
||||
}
|
||||
|
||||
if (window.Awesomplete && availableTags.length) {
|
||||
tagsAutocomplete = new window.Awesomplete(tagsTextInput, {
|
||||
list: getSuggestionList(),
|
||||
minChars: 1,
|
||||
maxItems: 8,
|
||||
autoFirst: true,
|
||||
sort: false,
|
||||
filter: window.Awesomplete.FILTER_CONTAINS,
|
||||
});
|
||||
|
||||
tagsTextInput.addEventListener('input', () => {
|
||||
refreshAutocomplete();
|
||||
tagsAutocomplete.evaluate();
|
||||
});
|
||||
|
||||
tagsTextInput.addEventListener('awesomplete-selectcomplete', commitInputValue);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', commitInputValue);
|
||||
|
||||
updateHiddenTags();
|
||||
syncReadLaterCheckbox();
|
||||
syncNoteCheckbox();
|
||||
renderTags();
|
||||
refreshAutocomplete();
|
||||
});
|
||||
}
|
||||
|
||||
initBookmarkEditorForms();
|
||||
|
||||
// ===== Persistent Media Player (Popup via Blob URL) =====
|
||||
// Audio plays in a separate popup window that survives page navigation.
|
||||
// The popup HTML is generated as a Blob URL (no server file needed).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user