From 0f2950c4cc6aff8cebb34d8a86b7a8e3e5fcd209 Mon Sep 17 00:00:00 2001 From: Lea Verou Date: Fri, 28 Mar 2025 17:06:58 -0400 Subject: [PATCH] Import CRUD parts from #828 --- docs/_layouts/palette.njk | 26 +++--- docs/assets/scripts/sidebar-tweaks.js | 73 +++++++++------- docs/assets/scripts/tweak/permalink.js | 103 +++++++++++++--------- docs/docs/palettes/tweak.js | 116 ++++++++++++++----------- docs/docs/themes/remix.js | 14 ++- 5 files changed, 199 insertions(+), 133 deletions(-) diff --git a/docs/_layouts/palette.njk b/docs/_layouts/palette.njk index d00d7c475..bc3f851a5 100644 --- a/docs/_layouts/palette.njk +++ b/docs/_layouts/palette.njk @@ -30,12 +30,21 @@ {% include 'breadcrumbs.njk' %} -

- {% raw %}{{ saved.title }}{% endraw %} - - +

+ {{ title }} +

-

{{ title }}

.wa-palette-{{ paletteId }} @@ -68,13 +77,6 @@ Reset - - - - - - Save - diff --git a/docs/assets/scripts/sidebar-tweaks.js b/docs/assets/scripts/sidebar-tweaks.js index 1c245717d..48b7076a5 100644 --- a/docs/assets/scripts/sidebar-tweaks.js +++ b/docs/assets/scripts/sidebar-tweaks.js @@ -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 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; }, }; diff --git a/docs/assets/scripts/tweak/permalink.js b/docs/assets/scripts/tweak/permalink.js index 27031dfd4..3d98b709e 100644 --- a/docs/assets/scripts/tweak/permalink.js +++ b/docs/assets/scripts/tweak/permalink.js @@ -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]; +} diff --git a/docs/docs/palettes/tweak.js b/docs/docs/palettes/tweak.js index 28b131210..e8824d1b9 100644 --- a/docs/docs/palettes/tweak.js +++ b/docs/docs/palettes/tweak.js @@ -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); }, diff --git a/docs/docs/themes/remix.js b/docs/docs/themes/remix.js index f997541f0..a6e9d8413 100644 --- a/docs/docs/themes/remix.js +++ b/docs/docs/themes/remix.js @@ -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(() => {