mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Import CRUD parts from #828
This commit is contained in:
@@ -30,12 +30,21 @@
|
||||
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
<h1 v-if="saved" class="title">
|
||||
{% raw %}{{ saved.title }}{% endraw %}
|
||||
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
|
||||
<wa-icon-button class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
|
||||
<h1 class="title">
|
||||
<span v-content="paletteTitle">{{ title }}</span>
|
||||
<template v-if="saved || tweaked">
|
||||
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
|
||||
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
|
||||
<wa-button @click="save()" :disabled="!unsavedChanges"
|
||||
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
|
||||
<span slot="prefix" class="icon-modifier">
|
||||
<wa-icon name="sidebar" variant="regular"></wa-icon>
|
||||
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
|
||||
</span>
|
||||
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
|
||||
</wa-button>
|
||||
</template>
|
||||
</h1>
|
||||
<h1 v-if="!saved" class="title">{{ title }}</h1>
|
||||
|
||||
<div class="block-info">
|
||||
<code class="class">.wa-palette-{{ paletteId }}</code>
|
||||
@@ -68,13 +77,6 @@
|
||||
</span>
|
||||
Reset
|
||||
</wa-button>
|
||||
<wa-button v-if="!saved" @click="save" variant="success">
|
||||
<span slot="prefix" class="icon-modifier">
|
||||
<wa-icon name="sidebar" variant="regular"></wa-icon>
|
||||
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
|
||||
</span>
|
||||
Save
|
||||
</wa-button>
|
||||
</wa-callout>
|
||||
|
||||
<table class="colors main wa-palette-{{ paletteId }}">
|
||||
|
||||
@@ -13,23 +13,33 @@ sidebar.palettes = {
|
||||
sidebar.updateCurrent();
|
||||
},
|
||||
|
||||
updateSaved() {
|
||||
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
|
||||
saved: [],
|
||||
|
||||
/**
|
||||
* Update saved palettes from local storage
|
||||
*/
|
||||
fromLocalStorage() {
|
||||
// Replace contents of array without breaking references
|
||||
let saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
|
||||
this.saved.splice(0, this.saved.length, ...saved);
|
||||
},
|
||||
|
||||
save(saved = this.saved) {
|
||||
this.saved = saved ?? [];
|
||||
|
||||
if (saved.length > 0) {
|
||||
localStorage.savedPalettes = JSON.stringify(saved);
|
||||
/**
|
||||
* Write palettes to local storage
|
||||
*/
|
||||
toLocalStorage() {
|
||||
if (this.saved.length > 0) {
|
||||
localStorage.savedPalettes = JSON.stringify(this.saved);
|
||||
} else {
|
||||
delete localStorage.savedPalettes;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
sidebar.palettes.updateSaved();
|
||||
addEventListener('storage', event => sidebar.palettes.updateSaved());
|
||||
sidebar.palettes.fromLocalStorage();
|
||||
|
||||
// Palettes were updated in another tab
|
||||
addEventListener('storage', () => sidebar.palettes.fromLocalStorage());
|
||||
|
||||
sidebar.palette = {
|
||||
getUid() {
|
||||
@@ -59,7 +69,9 @@ sidebar.palette = {
|
||||
delete(palette) {
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let count = savedPalettes.length;
|
||||
if (count === 0) {
|
||||
|
||||
if (count === 0 || !palette.uid) {
|
||||
// No stored palettes or this palette has not been saved
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +80,9 @@ sidebar.palette = {
|
||||
return;
|
||||
}
|
||||
|
||||
savedPalettes = savedPalettes.filter(p => !sidebar.palette.equals(palette, p));
|
||||
for (let index; (index = savedPalettes.findIndex(p => p.uid === palette.uid)) > -1; ) {
|
||||
savedPalettes.splice(index, 1);
|
||||
}
|
||||
|
||||
if (savedPalettes.length === count) {
|
||||
// Nothing was removed
|
||||
@@ -96,17 +110,14 @@ sidebar.palette = {
|
||||
|
||||
sidebar.updateCurrent();
|
||||
|
||||
sidebar.palettes.save(savedPalettes);
|
||||
sidebar.palettes.toLocalStorage();
|
||||
|
||||
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
|
||||
if (globalThis.paletteApp?.saved?.uid === palette.uid) {
|
||||
// We deleted the currently active palette
|
||||
paletteApp.postDelete();
|
||||
}
|
||||
},
|
||||
|
||||
getSaved(palette, savedPalettes = sidebar.palettes.saved) {
|
||||
return savedPalettes.find(p => sidebar.palette.equals(p, palette));
|
||||
},
|
||||
|
||||
render(palette) {
|
||||
// Find existing <a>
|
||||
let { title, id, search, uid } = palette;
|
||||
@@ -146,23 +157,27 @@ sidebar.palette = {
|
||||
}
|
||||
},
|
||||
|
||||
save(palette, saved) {
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let existing = this.getSaved(saved ?? palette, savedPalettes);
|
||||
let oldValues;
|
||||
|
||||
if (existing) {
|
||||
// Rename
|
||||
oldValues = { ...existing };
|
||||
Object.assign(existing, palette);
|
||||
} else {
|
||||
savedPalettes.push(palette);
|
||||
/**
|
||||
* Save a palette, either by updating its existing entry or creating a new one
|
||||
* @param {object} palette
|
||||
*/
|
||||
save(palette) {
|
||||
if (!palette.uid) {
|
||||
// First time saving
|
||||
palette.uid = this.getUid();
|
||||
}
|
||||
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let existingIndex = palette.uid ? sidebar.palettes.saved.findIndex(p => p.uid === palette.uid) : -1;
|
||||
let newIndex = existingIndex > -1 ? existingIndex : savedPalettes.length;
|
||||
|
||||
let [oldValues] = sidebar.palettes.saved.splice(newIndex, 1, palette);
|
||||
|
||||
this.render(palette, oldValues);
|
||||
sidebar.updateCurrent();
|
||||
sidebar.palettes.toLocalStorage();
|
||||
|
||||
sidebar.palettes.save(savedPalettes);
|
||||
return palette;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,56 +13,42 @@ export default class Permalink extends URLSearchParams {
|
||||
return Object.fromEntries(this.entries());
|
||||
}
|
||||
|
||||
#mappings = new WeakMap();
|
||||
|
||||
mapObject(obj, mapping = {}) {
|
||||
this.#mappings.set(obj, mapping);
|
||||
}
|
||||
|
||||
readFrom(obj) {
|
||||
let mapping = this.#mappings.get(obj) ?? {};
|
||||
let { keyFrom = IDENTITY, valueFrom = IDENTITY } = mapping;
|
||||
|
||||
for (let key in obj) {
|
||||
let value = obj[key];
|
||||
let mappedValue = valueFrom(value);
|
||||
let mappedKey = keyFrom(key);
|
||||
this.set(mappedKey, mappedValue);
|
||||
}
|
||||
}
|
||||
|
||||
writeTo(obj) {
|
||||
let mapping = this.#mappings.get(obj) ?? {};
|
||||
let { keyTo = IDENTITY, valueTo = IDENTITY, canExtend = false } = mapping;
|
||||
|
||||
for (let [key, value] of this) {
|
||||
let mappedKey = keyTo(key);
|
||||
let mappedValue = valueTo(value);
|
||||
|
||||
if (canExtend || mappedKey in obj) {
|
||||
obj[mappedKey] = mappedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value, defaultValue) {
|
||||
let oldValue = this.get(key);
|
||||
if (equals(value, defaultValue) || equals(value, '')) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
if (!value || value == defaultValue) {
|
||||
value ??= null; // undefined -> null
|
||||
|
||||
let oldValue = Array.isArray(value) ? this.getAll(key) : this.get(key);
|
||||
let changed = !equals(value, oldValue);
|
||||
|
||||
if (!changed) {
|
||||
// Nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
super.delete(key);
|
||||
value = value.slice();
|
||||
|
||||
if (oldValue) {
|
||||
this.changed = true;
|
||||
for (let v of value) {
|
||||
if (v || v === 0) {
|
||||
if (typeof v === 'object') {
|
||||
super.append(key, JSON.stringify(v));
|
||||
} else {
|
||||
super.append(key, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (value === null) {
|
||||
super.delete(key);
|
||||
} else {
|
||||
super.set(key, value);
|
||||
|
||||
if (String(value) !== String(oldValue)) {
|
||||
this.changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.sort();
|
||||
this.changed ||= changed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,3 +65,40 @@ export default class Permalink extends URLSearchParams {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function equals(value, oldValue) {
|
||||
if (Array.isArray(value) || Array.isArray(oldValue)) {
|
||||
value = toArray(value);
|
||||
oldValue = toArray(oldValue);
|
||||
|
||||
if (value.length !== oldValue.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.every((v, i) => equals(v, oldValue[i]));
|
||||
}
|
||||
|
||||
// (value ?? oldValue ?? true) returns true if they're both empty (null or undefined)
|
||||
[value, oldValue] = [value, oldValue].map(v => (!v && v !== false && v !== 0 ? null : v));
|
||||
return value === oldValue || String(value) === String(oldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to an array. `undefined` and `null` values are converted to an empty array.
|
||||
* @param {*} value - The value to convert.
|
||||
* @returns {any[]} The converted array.
|
||||
*/
|
||||
function toArray(value) {
|
||||
value ??= [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Don't convert "foo" into ["f", "o", "o"]
|
||||
if (typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
|
||||
return Array.from(value);
|
||||
}
|
||||
|
||||
return [value];
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ let paletteAppSpec = {
|
||||
return {
|
||||
uid: undefined,
|
||||
paletteId,
|
||||
paletteTitle: palette.title,
|
||||
originalPaletteTitle: palette.title,
|
||||
originalColors: palette.colors,
|
||||
permalink: new Permalink(),
|
||||
hueRanges,
|
||||
@@ -56,6 +56,8 @@ let paletteAppSpec = {
|
||||
grayColor: undefined,
|
||||
tweaking: {},
|
||||
saved: null,
|
||||
unsavedChanges: false,
|
||||
savedPalettes: sidebar.palettes.saved,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -63,20 +65,16 @@ let paletteAppSpec = {
|
||||
// Non-reactive variables to expose
|
||||
Object.assign(this, { moreHue });
|
||||
|
||||
// Read URL params and apply them. This facilitates permalinks.
|
||||
this.permalink.mapObject(this.hueShifts, {
|
||||
keyTo: key => key.replace(/-shift$/, ''),
|
||||
keyFrom: key => key + '-shift',
|
||||
valueFrom: value => (!value ? '' : Number(value)),
|
||||
valueTo: value => (!value ? 0 : Number(value)),
|
||||
});
|
||||
|
||||
this.grayChroma = this.originalGrayChroma;
|
||||
this.grayColor = this.originalGrayColor;
|
||||
|
||||
if (location.search) {
|
||||
// Update from URL
|
||||
this.permalink.writeTo(this.hueShifts);
|
||||
// Read URL params and apply them. This facilitates permalinks.
|
||||
for (let hue in this.hueShifts) {
|
||||
if (this.permalink.has(hue + '-shift')) {
|
||||
this.hueShifts[hue] = Number(this.permalink.get(hue + '-shift'));
|
||||
}
|
||||
}
|
||||
|
||||
for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
|
||||
if (this.permalink.has(param)) {
|
||||
@@ -94,9 +92,8 @@ let paletteAppSpec = {
|
||||
|
||||
if (this.permalink.has('uid')) {
|
||||
this.uid = Number(this.permalink.get('uid'));
|
||||
this.saved = sidebar.palettes.saved.find(p => p.uid === this.uid);
|
||||
}
|
||||
|
||||
this.saved = sidebar.palette.getSaved(this.getPalette());
|
||||
}
|
||||
},
|
||||
|
||||
@@ -104,9 +101,30 @@ let paletteAppSpec = {
|
||||
for (let ref in this.$refs) {
|
||||
this.$refs[ref].tooltipFormatter = percentFormatter;
|
||||
}
|
||||
|
||||
nextTick().then(() => {
|
||||
if (!this.tweaked || this.saved) {
|
||||
this.unsavedChanges = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
/** Default palette title for saving */
|
||||
defaultPaletteTitle() {
|
||||
return this.originalPaletteTitle + ' (tweaked)';
|
||||
},
|
||||
|
||||
paletteTitle() {
|
||||
if (this.saved) {
|
||||
return this.saved.title;
|
||||
} else if (this.tweaked) {
|
||||
return this.defaultPaletteTitle;
|
||||
} else {
|
||||
return this.originalPaletteTitle;
|
||||
}
|
||||
},
|
||||
|
||||
tweaks() {
|
||||
return {
|
||||
hueShifts: this.hueShifts,
|
||||
@@ -284,7 +302,9 @@ let paletteAppSpec = {
|
||||
hueShifts: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.permalink.readFrom(this.hueShifts);
|
||||
for (let hue in this.hueShifts) {
|
||||
this.permalink.set(hue + '-shift', this.hueShifts[hue], 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -308,58 +328,56 @@ let paletteAppSpec = {
|
||||
// Update page URL
|
||||
this.permalink.updateLocation();
|
||||
|
||||
if (this.saved) {
|
||||
this.save({ silent: true });
|
||||
}
|
||||
this.unsavedChanges = true;
|
||||
},
|
||||
},
|
||||
|
||||
saved: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.unsavedChanges = !this.saved;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getPalette() {
|
||||
return { id: this.paletteId, uid: this.uid, search: location.search };
|
||||
},
|
||||
|
||||
save({ silent } = {}) {
|
||||
let title = silent
|
||||
? (this.saved?.title ?? this.paletteTitle)
|
||||
: prompt('Palette title:', `${this.paletteTitle} (tweaked)`);
|
||||
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
|
||||
async save({ title } = {}) {
|
||||
let uid = this.uid;
|
||||
|
||||
if (!uid) {
|
||||
// First time saving
|
||||
this.uid = uid = sidebar.palette.getUid();
|
||||
this.saved ??= { id: this.paletteId, uid: this.uid };
|
||||
|
||||
this.permalink.set('uid', uid);
|
||||
this.permalink.updateLocation();
|
||||
if (title) {
|
||||
// Renaming
|
||||
this.saved.title = title;
|
||||
} else {
|
||||
this.saved.title ??= this.defaultPaletteTitle;
|
||||
}
|
||||
|
||||
let palette = { ...this.getPalette(), uid, title };
|
||||
this.saved.search = location.search;
|
||||
|
||||
sidebar.palette.save(palette, this.saved);
|
||||
this.saved = palette;
|
||||
this.saved = sidebar.palette.save(this.saved);
|
||||
|
||||
if (uid !== this.saved.uid) {
|
||||
// UID changed (most likely from saving a new palette)
|
||||
this.uid = this.saved.uid;
|
||||
this.permalink.set('uid', this.uid);
|
||||
this.permalink.updateLocation();
|
||||
await this.$nextTick();
|
||||
this.save(); // Save again to update the search param to include the UID
|
||||
}
|
||||
|
||||
this.unsavedChanges = false;
|
||||
},
|
||||
|
||||
rename() {
|
||||
if (!this.saved) {
|
||||
return;
|
||||
let newTitle = prompt('Palette title:', this.saved?.title ?? this.defaultPaletteTitle);
|
||||
|
||||
if (newTitle && newTitle !== this.saved?.title) {
|
||||
this.save({ title: newTitle });
|
||||
}
|
||||
|
||||
let newTitle = prompt('New title:', this.saved.title);
|
||||
|
||||
if (!newTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saved.title = newTitle;
|
||||
sidebar.palette.save(this.saved);
|
||||
},
|
||||
|
||||
// Cannot name this delete() because Vue complains
|
||||
deleteSaved() {
|
||||
sidebar.palette.delete(this.saved);
|
||||
},
|
||||
|
||||
14
docs/docs/themes/remix.js
vendored
14
docs/docs/themes/remix.js
vendored
@@ -54,8 +54,12 @@ function init() {
|
||||
urlParams: new Permalink(),
|
||||
};
|
||||
|
||||
data.urlParams.mapObject(data.params);
|
||||
data.urlParams.writeTo(data.params);
|
||||
// Apply params from permalink
|
||||
for (let key in data.params) {
|
||||
if (data.urlParams.has(key)) {
|
||||
data.params[key] = data.urlParams.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (computed.isRemixed) {
|
||||
// Start with the remixing UI open if the theme has been remixed
|
||||
@@ -128,7 +132,11 @@ function render(changedAspect) {
|
||||
selects[aspect].value = value;
|
||||
}
|
||||
|
||||
data.urlParams.readFrom(data.params);
|
||||
for (let key in data.params) {
|
||||
if (data.params[key]) {
|
||||
data.urlParams.set(key, data.params[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update demo URL
|
||||
domChange(() => {
|
||||
|
||||
Reference in New Issue
Block a user