mirror of
https://github.com/mue/mue.git
synced 2026-07-17 05:54:14 +02:00
feat(quicklinks): add drag-and-drop reordering for Quick Links (#1102)
* feat(quicklinks): enable drag-and-drop reordering for Quick Links * fix(quicklinks): some bug fixes
This commit is contained in:
@@ -5,17 +5,33 @@ import EventBus from 'utils/eventbus';
|
||||
|
||||
import './quicklinks.scss';
|
||||
|
||||
// Safe read to avoid crashing when localStorage has bad data
|
||||
const readQuicklinks = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('quicklinks');
|
||||
if (!raw) return [];
|
||||
const data = JSON.parse(raw);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
class QuickLinks extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
items: JSON.parse(localStorage.getItem('quicklinks')),
|
||||
items: readQuicklinks(),
|
||||
forceUpdate: 0, // Used to force complete re-render
|
||||
};
|
||||
this.quicklinksContainer = createRef();
|
||||
}
|
||||
|
||||
// widget zoom
|
||||
setZoom(element) {
|
||||
if (!element) return;
|
||||
|
||||
const zoom = localStorage.getItem('zoomQuicklinks') || 100;
|
||||
for (const link of element.getElementsByTagName('span')) {
|
||||
link.style.fontSize = `${14 * Number(zoom / 100)}px`;
|
||||
@@ -35,11 +51,13 @@ class QuickLinks extends PureComponent {
|
||||
return (this.quicklinksContainer.current.style.display = 'none');
|
||||
}
|
||||
|
||||
this.quicklinksContainer.current.style.display = 'block';
|
||||
this.setZoom(this.quicklinksContainer.current);
|
||||
|
||||
this.quicklinksContainer.current.style.display = 'flex';
|
||||
|
||||
this.setState({
|
||||
items: JSON.parse(localStorage.getItem('quicklinks')),
|
||||
items: readQuicklinks(),
|
||||
forceUpdate: Date.now(),
|
||||
}, () => {
|
||||
this.setZoom(this.quicklinksContainer.current);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -61,12 +79,12 @@ class QuickLinks extends PureComponent {
|
||||
|
||||
const tooltipEnabled = localStorage.getItem('quicklinkstooltip');
|
||||
|
||||
const quickLink = (item) => {
|
||||
const quickLink = (item, index) => {
|
||||
if (localStorage.getItem('quickLinksStyle') === 'text') {
|
||||
return (
|
||||
<a
|
||||
className="quicklinkstext"
|
||||
key={item.key}
|
||||
key={`quicklink-${item.key}-${index}`}
|
||||
href={item.url}
|
||||
target={target}
|
||||
rel={rel}
|
||||
@@ -79,13 +97,13 @@ class QuickLinks extends PureComponent {
|
||||
|
||||
const img =
|
||||
item.icon ||
|
||||
'https://icon.horse/icon/ ' + item.url.replace('https://', '').replace('http://', '');
|
||||
'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '');
|
||||
|
||||
if (localStorage.getItem('quickLinksStyle') === 'metro') {
|
||||
return (
|
||||
<a
|
||||
className="quickLinksMetro"
|
||||
key={item.key}
|
||||
key={`quicklink-${item.key}-${index}`}
|
||||
href={item.url}
|
||||
target={target}
|
||||
rel={rel}
|
||||
@@ -98,13 +116,13 @@ class QuickLinks extends PureComponent {
|
||||
}
|
||||
|
||||
const link = (
|
||||
<a key={item.key} href={item.url} target={target} rel={rel} draggable={false}>
|
||||
<a key={`quicklink-${item.key}-${index}`} href={item.url} target={target} rel={rel} draggable={false}>
|
||||
<img src={img} alt={item.name} draggable={false} />
|
||||
</a>
|
||||
);
|
||||
|
||||
return tooltipEnabled === 'true' ? (
|
||||
<Tooltip title={item.name} placement="bottom" key={item.key}>
|
||||
<Tooltip title={item.name} placement="bottom" key={`quicklink-${item.key}-${index}`}>
|
||||
{link}
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -113,11 +131,14 @@ class QuickLinks extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="quicklinkscontainer" ref={this.quicklinksContainer}>
|
||||
{this.state.items.map((item) => quickLink(item))}
|
||||
<div
|
||||
className="quicklinkscontainer"
|
||||
ref={this.quicklinksContainer}
|
||||
>
|
||||
{this.state.items && this.state.items.map((item, index) => quickLink(item, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { QuickLinks as default, QuickLinks };
|
||||
export { QuickLinks as default, QuickLinks };
|
||||
@@ -1,9 +1,20 @@
|
||||
import variables from 'config/variables';
|
||||
|
||||
import { MdEdit, MdCancel } from 'react-icons/md';
|
||||
import { MdEdit, MdCancel, MdDragHandle } from 'react-icons/md';
|
||||
import { Button } from 'components/Elements';
|
||||
|
||||
const QuickLink = ({ item, deleteLink, startEditLink }) => {
|
||||
const QuickLink = ({
|
||||
item,
|
||||
deleteLink,
|
||||
startEditLink,
|
||||
draggable = false,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
isDragOver = false,
|
||||
isDragging = false,
|
||||
}) => {
|
||||
let target,
|
||||
rel = null;
|
||||
if (localStorage.getItem('quicklinksnewtab') === 'true') {
|
||||
@@ -32,8 +43,22 @@ const QuickLink = ({ item, deleteLink, startEditLink }) => {
|
||||
item.icon ||
|
||||
'https://icon.horse/icon/ ' + item.url.replace('https://', '').replace('http://', '');
|
||||
|
||||
// Compose classes for drag state
|
||||
const rootClass = `messageMap ${isDragging ? 'dragging' : ''} ${isDragOver ? 'drag-over' : ''}`;
|
||||
|
||||
return (
|
||||
<div className="messageMap">
|
||||
<div
|
||||
className={rootClass}
|
||||
draggable={draggable}
|
||||
onDragStart={(e) => onDragStart && onDragStart(e, item.key)}
|
||||
onDragOver={(e) => onDragOver && onDragOver(e, item.key)}
|
||||
onDrop={(e) => onDrop && onDrop(e, item.key)}
|
||||
onDragEnd={(e) => onDragEnd && onDragEnd(e)}
|
||||
>
|
||||
<div className="dragHandle" aria-hidden>
|
||||
<MdDragHandle />
|
||||
</div>
|
||||
|
||||
<div className="icon">
|
||||
<img
|
||||
src={img}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { MdAddLink, MdLinkOff } from 'react-icons/md';
|
||||
import { MdAddLink, MdLinkOff, MdOutlineDragIndicator, MdEdit, MdDelete } from 'react-icons/md';
|
||||
import { sortableContainer, sortableElement } from '@muetab/react-sortable-hoc';
|
||||
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
|
||||
import { Checkbox, Dropdown } from 'components/Form/Settings';
|
||||
import { Button } from 'components/Elements';
|
||||
@@ -9,194 +10,296 @@ import Modal from 'react-modal';
|
||||
import { AddModal } from 'components/Elements/AddModal';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { QuickLink } from './QuickLink';
|
||||
import { getTitleFromUrl, isValidUrl } from 'utils/links';
|
||||
|
||||
const readQuicklinks = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('quicklinks');
|
||||
if (!raw) return [];
|
||||
const data = JSON.parse(raw);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const DragHandle = () => (
|
||||
<div className="quicklink-drag-handle" aria-hidden="true">
|
||||
<MdOutlineDragIndicator />
|
||||
</div>
|
||||
);
|
||||
|
||||
const SortableItem = sortableElement(({ value, enabled, startEditLink, deleteLink }) => {
|
||||
const getIconUrl = (item) => {
|
||||
return item.icon || 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`quicklink-item ${!enabled ? 'disabled' : ''}`} role="listitem">
|
||||
<DragHandle />
|
||||
<div className="quicklink-icon">
|
||||
<img src={getIconUrl(value)} alt={value.name} draggable={false} />
|
||||
</div>
|
||||
<div className="quicklink-content">
|
||||
<div className="quicklink-name">{value.name}</div>
|
||||
<div className="quicklink-url">{value.url}</div>
|
||||
</div>
|
||||
<div className="quicklink-actions">
|
||||
<button
|
||||
className="quicklink-action-btn"
|
||||
onClick={(e) => {
|
||||
if (!enabled) return;
|
||||
e.stopPropagation();
|
||||
startEditLink(value);
|
||||
}}
|
||||
title="Edit"
|
||||
disabled={!enabled}
|
||||
aria-disabled={!enabled}
|
||||
>
|
||||
<MdEdit />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
className="quicklink-action-btn quicklink-remove-btn"
|
||||
onClick={(e) => {
|
||||
if (!enabled) return;
|
||||
e.stopPropagation();
|
||||
deleteLink(value.key, e);
|
||||
}}
|
||||
title="Remove"
|
||||
disabled={!enabled}
|
||||
aria-disabled={!enabled}
|
||||
>
|
||||
<MdDelete />
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SortableContainer = sortableContainer(({ children }) => (
|
||||
<div className="quicklinks-list" role="list">{children}</div>
|
||||
));
|
||||
|
||||
class QuickLinksOptions extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
items: JSON.parse(localStorage.getItem('quicklinks')),
|
||||
items: readQuicklinks(),
|
||||
showAddModal: false,
|
||||
urlError: '',
|
||||
iconError: '',
|
||||
edit: false,
|
||||
editData: '',
|
||||
enabled: localStorage.getItem('quicklinksenabled') !== 'false',
|
||||
};
|
||||
this.quicklinksContainer = createRef();
|
||||
this.silenceEvent = false;
|
||||
this.handleRefresh = null;
|
||||
}
|
||||
|
||||
setContainerDisplay(enabled) {
|
||||
if (!this.quicklinksContainer || !this.quicklinksContainer.current) return;
|
||||
const el = this.quicklinksContainer.current;
|
||||
el.classList.toggle('disabled', !enabled);
|
||||
if (!enabled) {
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
el.removeAttribute('aria-hidden');
|
||||
}
|
||||
}
|
||||
deleteLink(key, event) {
|
||||
event.preventDefault();
|
||||
|
||||
// remove link from array
|
||||
const data = JSON.parse(localStorage.getItem('quicklinks')).filter((i) => i.key !== key);
|
||||
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({
|
||||
items: data,
|
||||
});
|
||||
event.preventDefault();
|
||||
|
||||
const stored = readQuicklinks();
|
||||
const data = stored.filter((i) => i.key !== key);
|
||||
this.silenceEvent = true;
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({ items: data }, () => {
|
||||
variables.stats.postEvent('feature', 'Quicklink delete');
|
||||
}
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => { this.silenceEvent = false; }, 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async addLink(name, url, icon) {
|
||||
const data = JSON.parse(localStorage.getItem('quicklinks'));
|
||||
const data = readQuicklinks();
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
if (url.length <= 0 || isValidUrl(url) === false) {
|
||||
return this.setState({
|
||||
urlError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||
});
|
||||
}
|
||||
|
||||
if (icon.length > 0 && isValidUrl(icon) === false) {
|
||||
return this.setState({
|
||||
iconError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||
});
|
||||
}
|
||||
|
||||
data.push({
|
||||
name: name || (await getTitleFromUrl(url)),
|
||||
url,
|
||||
icon: icon || '',
|
||||
key: Math.random().toString(36).substring(7) + 1,
|
||||
});
|
||||
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
|
||||
this.setState({
|
||||
items: data,
|
||||
showAddModal: false,
|
||||
urlError: '',
|
||||
iconError: '',
|
||||
});
|
||||
|
||||
variables.stats.postEvent('feature', 'Quicklink add');
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
if (url.length <= 0 || isValidUrl(url) === false) {
|
||||
return this.setState({ urlError: variables.getMessage('widgets.quicklinks.url_error') });
|
||||
}
|
||||
|
||||
if (icon.length > 0 && isValidUrl(icon) === false) {
|
||||
return this.setState({ iconError: variables.getMessage('widgets.quicklinks.url_error') });
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
name: name || (await getTitleFromUrl(url)),
|
||||
url,
|
||||
icon: icon || '',
|
||||
key: Date.now().toString() + Math.random().toString(36).substring(2),
|
||||
};
|
||||
|
||||
data.push(newItem);
|
||||
|
||||
this.silenceEvent = true;
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({ items: data, showAddModal: false, urlError: '', iconError: '' }, () => {
|
||||
variables.stats.postEvent('feature', 'Quicklink add');
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => { this.silenceEvent = false; }, 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
startEditLink(data) {
|
||||
this.setState({
|
||||
edit: true,
|
||||
editData: data,
|
||||
showAddModal: true,
|
||||
});
|
||||
this.setState({ edit: true, editData: data, showAddModal: true });
|
||||
}
|
||||
|
||||
async editLink(og, name, url, icon) {
|
||||
const data = JSON.parse(localStorage.getItem('quicklinks'));
|
||||
const dataobj = data.find((i) => i.key === og.key);
|
||||
dataobj.name = name || (await getTitleFromUrl(url));
|
||||
dataobj.url = url;
|
||||
dataobj.icon = icon || '';
|
||||
const data = readQuicklinks();
|
||||
const dataobj = data.find((i) => i.key === og.key);
|
||||
if (!dataobj) return;
|
||||
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
dataobj.name = name || (await getTitleFromUrl(url));
|
||||
dataobj.url = url;
|
||||
dataobj.icon = icon || '';
|
||||
|
||||
this.setState({
|
||||
items: data,
|
||||
showAddModal: false,
|
||||
edit: false,
|
||||
});
|
||||
this.silenceEvent = true;
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({ items: data, showAddModal: false, edit: false }, () => {
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => { this.silenceEvent = false; }, 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
arrayMove = (array, oldIndex, newIndex) => {
|
||||
const result = Array.from(array);
|
||||
const [removed] = result.splice(oldIndex, 1);
|
||||
result.splice(newIndex, 0, removed);
|
||||
return result;
|
||||
};
|
||||
|
||||
onSortStart = () => {
|
||||
if (this.quicklinksContainer && this.quicklinksContainer.current) {
|
||||
this.quicklinksContainer.current.classList.add('dragging');
|
||||
}
|
||||
};
|
||||
|
||||
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
if (!this.state.enabled) return;
|
||||
if (oldIndex === newIndex) return;
|
||||
const newItems = this.arrayMove(this.state.items, oldIndex, newIndex);
|
||||
|
||||
this.silenceEvent = true;
|
||||
this.setState({ items: newItems }, () => {
|
||||
localStorage.setItem('quicklinks', JSON.stringify(newItems));
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => { this.silenceEvent = false; }, 0);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'quicklinks') {
|
||||
if (localStorage.getItem('quicklinksenabled') === 'false') {
|
||||
return (this.quicklinksContainer.current.style.display = 'none');
|
||||
}
|
||||
this.setContainerDisplay(this.state.enabled);
|
||||
|
||||
this.quicklinksContainer.current.style.display = 'block';
|
||||
this.handleRefresh = (data) => {
|
||||
if (data !== 'quicklinks') return;
|
||||
if (this.silenceEvent) return;
|
||||
|
||||
this.setState({
|
||||
items: JSON.parse(localStorage.getItem('quicklinks')),
|
||||
});
|
||||
}
|
||||
});
|
||||
const enabled = localStorage.getItem('quicklinksenabled') !== 'false';
|
||||
const newItems = readQuicklinks();
|
||||
const oldItems = this.state.items || [];
|
||||
const oldKeys = new Set(oldItems.map(i => i.key));
|
||||
const newKeys = new Set(newItems.map(i => i.key));
|
||||
|
||||
const keysEqual =
|
||||
oldItems.length === newItems.length &&
|
||||
oldItems.every(i => newKeys.has(i.key));
|
||||
|
||||
if (enabled !== this.state.enabled || !keysEqual) {
|
||||
this.setContainerDisplay(enabled);
|
||||
this.setState({ items: newItems, enabled });
|
||||
}
|
||||
};
|
||||
|
||||
EventBus.on('refresh', this.handleRefresh);
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.enabled !== this.state.enabled) {
|
||||
this.setContainerDisplay(this.state.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
componentWillUnmount() {
|
||||
if (this.handleRefresh) {
|
||||
EventBus.off('refresh', this.handleRefresh);
|
||||
} else {
|
||||
try { EventBus.off('refresh'); } catch (e) {}
|
||||
}
|
||||
|
||||
}
|
||||
render() {
|
||||
const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks';
|
||||
const { enabled } = this.state;
|
||||
|
||||
const AdditionalSettings = () => {
|
||||
return (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${QUICKLINKS_SECTION}.additional`)}
|
||||
const AdditionalSettings = () => (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${QUICKLINKS_SECTION}.additional`)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox name="quicklinksnewtab" text={variables.getMessage(`${QUICKLINKS_SECTION}.open_new`)} category="quicklinks" />
|
||||
<Checkbox name="quicklinkstooltip" text={variables.getMessage(`${QUICKLINKS_SECTION}.tooltip`)} category="quicklinks" />
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const StylingOptions = () => (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUICKLINKS_SECTION}.styling`)}
|
||||
subtitle={variables.getMessage('modals.main.settings.sections.quicklinks.styling_description')}
|
||||
/>
|
||||
<Action>
|
||||
<Dropdown
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.style`)}
|
||||
name="quickLinksStyle"
|
||||
category="quicklinks"
|
||||
items={[
|
||||
{ value: 'icon', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.icon`) },
|
||||
{ value: 'text_only', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.text_only`) },
|
||||
{ value: 'metro', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.metro`) },
|
||||
]}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="quicklinksnewtab"
|
||||
text={variables.getMessage(`${QUICKLINKS_SECTION}.open_new`)}
|
||||
category="quicklinks"
|
||||
/>
|
||||
<Checkbox
|
||||
name="quicklinkstooltip"
|
||||
text={variables.getMessage(`${QUICKLINKS_SECTION}.tooltip`)}
|
||||
category="quicklinks"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const StylingOptions = () => {
|
||||
return (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUICKLINKS_SECTION}.styling`)}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.quicklinks.styling_description',
|
||||
)}
|
||||
const AddLink = () => (
|
||||
<Row final={true}>
|
||||
<Content title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)} />
|
||||
<Action>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => enabled && this.setState({ showAddModal: true })}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
<Action>
|
||||
<Dropdown
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.style`)}
|
||||
name="quickLinksStyle"
|
||||
category="quicklinks"
|
||||
items={[
|
||||
{
|
||||
value: 'icon',
|
||||
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.icon`),
|
||||
},
|
||||
{
|
||||
value: 'text',
|
||||
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.text_only`),
|
||||
},
|
||||
{
|
||||
value: 'metro',
|
||||
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.metro`),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const AddLink = () => {
|
||||
return (
|
||||
<Row final={true}>
|
||||
<Content title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)} />
|
||||
<Action>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.setState({ showAddModal: true })}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -208,6 +311,7 @@ class QuickLinksOptions extends PureComponent {
|
||||
zoomSetting="zoomQuicklinks"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
|
||||
<PreferencesWrapper
|
||||
setting="quicklinksenabled"
|
||||
category="quicklinks"
|
||||
@@ -222,33 +326,43 @@ class QuickLinksOptions extends PureComponent {
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdLinkOff />
|
||||
<span className="title">
|
||||
{variables.getMessage(`${QUICKLINKS_SECTION}.no_quicklinks`)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.message.add_some')}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.setState({ showAddModal: true })}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
/>
|
||||
<span className="title">{variables.getMessage(`${QUICKLINKS_SECTION}.no_quicklinks`)}</span>
|
||||
<span className="subtitle">{variables.getMessage('modals.main.settings.sections.message.add_some')}</span>
|
||||
<Button type="settings" onClick={() => this.setState({ showAddModal: true })} icon={<MdAddLink />} label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PreferencesWrapper>
|
||||
|
||||
<div className="messagesContainer" ref={this.quicklinksContainer}>
|
||||
{this.state.items.map((item, i) => (
|
||||
<QuickLink
|
||||
key={i}
|
||||
item={item}
|
||||
startEditLink={() => this.startEditLink(item)}
|
||||
deleteLink={(key, e) => this.deleteLink(key, e)}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className={`quicklinks-container ${!enabled ? 'disabled' : ''}`}
|
||||
ref={this.quicklinksContainer}
|
||||
aria-hidden={!enabled}
|
||||
>
|
||||
<div className={`messagesContainer ${!enabled ? 'disabled' : ''}`}>
|
||||
<SortableContainer
|
||||
onSortStart={this.onSortStart}
|
||||
onSortEnd={this.onSortEnd}
|
||||
lockAxis="y"
|
||||
lockToContainerEdges
|
||||
disableAutoscroll
|
||||
helperClass="sortable-helper"
|
||||
distance={6}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{this.state.items.map((item, index) => (
|
||||
<SortableItem
|
||||
key={item.key}
|
||||
index={index}
|
||||
value={item}
|
||||
enabled={enabled}
|
||||
startEditLink={(data) => this.startEditLink(data)}
|
||||
deleteLink={(key, e) => this.deleteLink(key, e)}
|
||||
/>
|
||||
))}
|
||||
</SortableContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => this.setState({ showAddModal: false, urlError: '', iconError: '' })}
|
||||
@@ -263,9 +377,7 @@ class QuickLinksOptions extends PureComponent {
|
||||
editLink={(og, name, url, icon) => this.editLink(og, name, url, icon)}
|
||||
edit={this.state.edit}
|
||||
editData={this.state.editData}
|
||||
closeModal={() =>
|
||||
this.setState({ showAddModal: false, urlError: '', iconError: '', edit: false })
|
||||
}
|
||||
closeModal={() => this.setState({ showAddModal: false, urlError: '', iconError: '', edit: false })}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
@@ -273,4 +385,4 @@ class QuickLinksOptions extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export { QuickLinksOptions as default, QuickLinksOptions };
|
||||
export { QuickLinksOptions as default, QuickLinksOptions };
|
||||
@@ -24,6 +24,13 @@
|
||||
flex-wrap: wrap;
|
||||
padding: 0 25px;
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
overflow-x: visible;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.sortable-quicklinks {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@extend %basic;
|
||||
@@ -60,25 +67,75 @@
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinks-container > a,
|
||||
.quicklinks-container > .quicklinks > button {
|
||||
display: inline;
|
||||
.messagesContainer.disabled {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.quicklinks-container {
|
||||
img {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
transition: transform 0.2s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
.quicklink-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
||||
.quicklink-drag-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 5px;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklink-drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.metro-wrapper {
|
||||
flex-direction: column;
|
||||
|
||||
.quicklink-drag-handle {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +149,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* background-color: var(--background);
|
||||
color: var(--modal-text); */
|
||||
|
||||
.quicklinksdropdown {
|
||||
@extend %basic;
|
||||
|
||||
@@ -258,3 +312,315 @@ button.quicklinks {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
.messageMap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
transition: box-shadow 0.15s ease, transform 0.12s ease, opacity 0.12s ease;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.995);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
outline: 2px dashed rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-size: 20px;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
margin-right: 4px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
.messageText {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.messageAction {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinks-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quicklink-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
transition: all 0.01s ease;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
|
||||
.quicklink-drag-handle {
|
||||
opacity: 1;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.quicklink-drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 24px;
|
||||
padding: 4px;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.quicklink-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quicklink-icon-placeholder {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklink-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.quicklink-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.quicklink-url {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklink-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.quicklink-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.quicklink-remove-btn {
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quicklink-item.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.quicklink-item.disabled .quicklink-action-btn {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.quicklink-item.disabled .drag-handle {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sortable-helper {
|
||||
transform: scale(1.02);
|
||||
z-index: 9999 !important;
|
||||
opacity: 0.5 !important;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5) !important;
|
||||
cursor: grabbing !important;
|
||||
|
||||
|
||||
&.quicklink-item {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.quicklink-drag-handle {
|
||||
opacity: 1 !important;
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
.quicklink-icon,
|
||||
.quicklink-content,
|
||||
.quicklink-actions {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
img {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.messagesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.quicklink-wrapper .quicklinkstext {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-size: 0.8em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.quicklink-wrapper .quickLinksMetro {
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
|
||||
.subtitle {
|
||||
margin-top: 8px;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user