mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
* Exclude Create link from sidebar, for reals this time * Fix bug * Very rough prototype of look & feel * a11y * Clean up data files * Automatically generate theme metadata * Read look & feel params straight from theme * First stab at dimensionality icons * Fix rounding 0 bug * Add border width slider * [Image-comparer] Expose wrapper as part * [Comparer] `pointer-events: none` while dragging * Dark mode slider * Adjust increments and ranges for look + feel sliders * Fix preview * Fix bug where dark mode was not inverted * Ability to select panel from URL * Create mixin for Vue form controls and use it in `<swatch-select>` * Prototype of slider min/max icon buttons * Nx tooltip * Icons * Prevent failed request * info-tip: Support passing text as prop * Clearable * [Brutalist] Match `--wa-shadow-offset-x-scale` to `--wa-shadow-offset-y-scale` * Add 'Blocky' dimension (derived from Awesome theme) * Only show Reset button when `clearable` is set * Remove `clearable` from Look & Feel sliders * Add tooltips to min/max buttons * Remove superfluous `aria-label` * Do not assume that all hyphens in URLs mean nesting, make it explicit * Formatting * Fix bug where styles were not applied on page load * Update Subtle dimension to maximize compatibility * `<wa-scoped>`: Do not allow non-template children * Workaround for card not updating * Update Glossy dimension to maximize compatibility * Sync scrolling between regular and inverted preview * Fix bug * Make changing the base theme reset customizations * Fix palette page * Remove cancel button from editable text * Don't error in theme pages * Update Playful dimension to maximize compatibility * Rename 'Look and Feel' to 'Elements' for better parallel structure * Hide dimensionality controls * Make back icon motion more subtle * Expand spacing slider bounds * Add `tabindex="-1"` where missing in theme showcase * Remove extraneous gap from theme headers * fix edit button bug * rename comparer => comparison; fix aria-controls * Always save theme name on blur * Add changelog for themer and new patterns category --------- Co-authored-by: lindsaym-fa <dev@lindsaym.design> Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
179 lines
4.4 KiB
JavaScript
179 lines
4.4 KiB
JavaScript
import { deepEach, deepGet, deepSet } from './util/deep.js';
|
||
import { camelCase, kebabCase } from './util/string.js';
|
||
|
||
export default class Permalink extends URLSearchParams {
|
||
/** Params changed since last URL I/O */
|
||
changed = false;
|
||
|
||
constructor(params) {
|
||
super(location.search);
|
||
this.params = params;
|
||
}
|
||
|
||
toJSON() {
|
||
return Object.fromEntries(this.entries());
|
||
}
|
||
|
||
/**
|
||
* Set multiple values from an object. Nested values will be joined with a hyphen.
|
||
* @param {object} values - The object containing the values to set.
|
||
* @param {object} defaults - The object containing the default values.
|
||
*
|
||
*/
|
||
setAll(values, defaults) {
|
||
deepEach(values, (value, key, parent, path) => {
|
||
let fullPath = [...path, key];
|
||
let param = fullPath.map(kebabCase).join('-');
|
||
|
||
if (typeof value === 'object') {
|
||
// We'll handle this when we descend into it
|
||
return;
|
||
}
|
||
|
||
let defaultValue = deepGet(defaults, fullPath);
|
||
|
||
if (equals(value, defaultValue)) {
|
||
// Remove the param from the URL
|
||
this.delete(param);
|
||
return;
|
||
}
|
||
|
||
this.set(param, value);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Convert the URL params to a (potentially nested) object.
|
||
* @param {object} options - Options object.
|
||
* @param {(key: string, value: string) => string[]} options.getPath - Function to get the path of a param.
|
||
* @returns {object} The nested object.
|
||
*/
|
||
toObject(options = {}) {
|
||
// Default getPath() assumes hyphens always mean nesting
|
||
let { ignoreKeys = [], getPath = param => param.split('-') } = options;
|
||
|
||
// Get all values as a nested object
|
||
|
||
let obj = {};
|
||
|
||
for (let [key, value] of this.entries()) {
|
||
let path = getPath(key, value);
|
||
|
||
if (path === null || ignoreKeys.includes(key)) {
|
||
// Skip this param
|
||
continue;
|
||
}
|
||
|
||
// Default to key if `getPath()` returns undefined
|
||
path ??= key;
|
||
|
||
path = Array.isArray(path) ? path : [path];
|
||
|
||
// Camel case any remaining hyphens
|
||
path = path.map(camelCase);
|
||
|
||
deepSet(obj, path, value);
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
delete(key, value) {
|
||
let hadValue = this.has(key);
|
||
super.delete(key, value);
|
||
|
||
if (hadValue) {
|
||
this.changed = true;
|
||
}
|
||
}
|
||
|
||
set(key, value, defaultValue) {
|
||
if (equals(value, defaultValue) || equals(value, '')) {
|
||
value = null;
|
||
}
|
||
|
||
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();
|
||
|
||
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);
|
||
}
|
||
|
||
this.sort();
|
||
this.changed ||= changed;
|
||
}
|
||
|
||
/**
|
||
* Update page URL if it has changed since last time
|
||
*/
|
||
updateLocation() {
|
||
if (this.changed) {
|
||
// If there’s already a search, replace it.
|
||
// We don’t want to clog the user’s history while they iterate
|
||
let search = this.toString();
|
||
let historyAction = location.search && search ? 'replaceState' : 'pushState';
|
||
history[historyAction](null, '', `?${search}`);
|
||
this.changed = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
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];
|
||
}
|