| 1 |
(function() { |
| 2 |
"use strict"; |
| 3 |
function html(strings, ...values) { |
| 4 |
return { __wpdHtml: true, strings, values }; |
| 5 |
} |
| 6 |
function isTemplateResult(v) { |
| 7 |
return !!v && v.__wpdHtml === true; |
| 8 |
} |
| 9 |
const MARKER_PREFIX = "$$wpd$$"; |
| 10 |
const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g; |
| 11 |
function joinWithMarkers(strings) { |
| 12 |
let out = strings[0]; |
| 13 |
for (let i = 1; i < strings.length; i++) { |
| 14 |
out += `${MARKER_PREFIX}${i - 1}$$` + strings[i]; |
| 15 |
} |
| 16 |
return out; |
| 17 |
} |
| 18 |
const compiledCache = /* @__PURE__ */ new WeakMap(); |
| 19 |
function compile(strings) { |
| 20 |
const cached = compiledCache.get(strings); |
| 21 |
if (cached) { |
| 22 |
return cached; |
| 23 |
} |
| 24 |
const template = document.createElement("template"); |
| 25 |
template.innerHTML = joinWithMarkers(strings); |
| 26 |
const recipes = []; |
| 27 |
const walk = (node, path) => { |
| 28 |
if (node.nodeType === Node.ELEMENT_NODE) { |
| 29 |
const el = node; |
| 30 |
for (const attr of Array.from(el.attributes)) { |
| 31 |
const rawName = attr.name; |
| 32 |
const rawValue = attr.value; |
| 33 |
const prefix = rawName[0]; |
| 34 |
if (MARKER_RE.test(rawValue)) { |
| 35 |
MARKER_RE.lastIndex = 0; |
| 36 |
if (prefix === "@") { |
| 37 |
const match = MARKER_RE.exec(rawValue); |
| 38 |
MARKER_RE.lastIndex = 0; |
| 39 |
recipes.push({ |
| 40 |
path, |
| 41 |
kind: "event", |
| 42 |
name: rawName.slice(1), |
| 43 |
valueIndex: match ? Number(match[1]) : 0 |
| 44 |
}); |
| 45 |
el.removeAttribute(rawName); |
| 46 |
} else if (prefix === ".") { |
| 47 |
const match = MARKER_RE.exec(rawValue); |
| 48 |
MARKER_RE.lastIndex = 0; |
| 49 |
recipes.push({ |
| 50 |
path, |
| 51 |
kind: "prop", |
| 52 |
name: rawName.slice(1), |
| 53 |
valueIndex: match ? Number(match[1]) : 0 |
| 54 |
}); |
| 55 |
el.removeAttribute(rawName); |
| 56 |
} else if (prefix === "?") { |
| 57 |
const match = MARKER_RE.exec(rawValue); |
| 58 |
MARKER_RE.lastIndex = 0; |
| 59 |
recipes.push({ |
| 60 |
path, |
| 61 |
kind: "bool", |
| 62 |
name: rawName.slice(1), |
| 63 |
valueIndex: match ? Number(match[1]) : 0 |
| 64 |
}); |
| 65 |
el.removeAttribute(rawName); |
| 66 |
} else { |
| 67 |
const fragments = []; |
| 68 |
const indices = []; |
| 69 |
let lastEnd = 0; |
| 70 |
let m; |
| 71 |
MARKER_RE.lastIndex = 0; |
| 72 |
while ((m = MARKER_RE.exec(rawValue)) !== null) { |
| 73 |
fragments.push(rawValue.slice(lastEnd, m.index)); |
| 74 |
indices.push(Number(m[1])); |
| 75 |
lastEnd = m.index + m[0].length; |
| 76 |
} |
| 77 |
fragments.push(rawValue.slice(lastEnd)); |
| 78 |
recipes.push({ |
| 79 |
path, |
| 80 |
kind: "attr", |
| 81 |
name: rawName, |
| 82 |
template: fragments, |
| 83 |
valueIndices: indices |
| 84 |
}); |
| 85 |
el.setAttribute(rawName, ""); |
| 86 |
} |
| 87 |
} |
| 88 |
} |
| 89 |
} |
| 90 |
const children = Array.from(node.childNodes); |
| 91 |
let shift = 0; |
| 92 |
for (let i = 0; i < children.length; i++) { |
| 93 |
const child = children[i]; |
| 94 |
const liveIndex = i + shift; |
| 95 |
if (child.nodeType === Node.TEXT_NODE) { |
| 96 |
const text = child.textContent || ""; |
| 97 |
if (!MARKER_RE.test(text)) { |
| 98 |
MARKER_RE.lastIndex = 0; |
| 99 |
continue; |
| 100 |
} |
| 101 |
MARKER_RE.lastIndex = 0; |
| 102 |
const parent = child.parentNode; |
| 103 |
let lastEnd = 0; |
| 104 |
let m; |
| 105 |
const newNodes = []; |
| 106 |
const newRecipes = []; |
| 107 |
MARKER_RE.lastIndex = 0; |
| 108 |
while ((m = MARKER_RE.exec(text)) !== null) { |
| 109 |
if (m.index > lastEnd) { |
| 110 |
newNodes.push(document.createTextNode(text.slice(lastEnd, m.index))); |
| 111 |
} |
| 112 |
const placeholder = document.createTextNode(""); |
| 113 |
newNodes.push(placeholder); |
| 114 |
newRecipes.push({ |
| 115 |
path: [...path, liveIndex + newNodes.length - 1], |
| 116 |
kind: "node", |
| 117 |
valueIndex: Number(m[1]) |
| 118 |
}); |
| 119 |
lastEnd = m.index + m[0].length; |
| 120 |
} |
| 121 |
if (lastEnd < text.length) { |
| 122 |
newNodes.push(document.createTextNode(text.slice(lastEnd))); |
| 123 |
} |
| 124 |
for (const nn of newNodes) { |
| 125 |
parent.insertBefore(nn, child); |
| 126 |
} |
| 127 |
parent.removeChild(child); |
| 128 |
shift += newNodes.length - 1; |
| 129 |
recipes.push(...newRecipes); |
| 130 |
} else { |
| 131 |
walk(child, [...path, liveIndex]); |
| 132 |
} |
| 133 |
} |
| 134 |
}; |
| 135 |
walk(template.content, []); |
| 136 |
const buildParts = (fragment) => { |
| 137 |
const out = []; |
| 138 |
for (const r of recipes) { |
| 139 |
let node = fragment; |
| 140 |
for (const idx of r.path) { |
| 141 |
node = node.childNodes[idx]; |
| 142 |
} |
| 143 |
if (r.kind === "node") { |
| 144 |
out.push({ |
| 145 |
kind: "node", |
| 146 |
valueIndex: r.valueIndex, |
| 147 |
child: { |
| 148 |
anchor: node, |
| 149 |
state: null |
| 150 |
} |
| 151 |
}); |
| 152 |
} else if (r.kind === "attr") { |
| 153 |
out.push({ |
| 154 |
kind: "attr", |
| 155 |
element: node, |
| 156 |
name: r.name, |
| 157 |
template: r.template, |
| 158 |
valueIndices: r.valueIndices |
| 159 |
}); |
| 160 |
} else if (r.kind === "event") { |
| 161 |
out.push({ |
| 162 |
kind: "event", |
| 163 |
valueIndex: r.valueIndex, |
| 164 |
element: node, |
| 165 |
name: r.name |
| 166 |
}); |
| 167 |
} else if (r.kind === "prop") { |
| 168 |
out.push({ |
| 169 |
kind: "prop", |
| 170 |
valueIndex: r.valueIndex, |
| 171 |
element: node, |
| 172 |
name: r.name |
| 173 |
}); |
| 174 |
} else if (r.kind === "bool") { |
| 175 |
out.push({ |
| 176 |
kind: "bool", |
| 177 |
valueIndex: r.valueIndex, |
| 178 |
element: node, |
| 179 |
name: r.name |
| 180 |
}); |
| 181 |
} |
| 182 |
} |
| 183 |
return out; |
| 184 |
}; |
| 185 |
const entry = { template, buildParts }; |
| 186 |
compiledCache.set(strings, entry); |
| 187 |
return entry; |
| 188 |
} |
| 189 |
const mountState = /* @__PURE__ */ new WeakMap(); |
| 190 |
function mountIntact(state, container) { |
| 191 |
for (const node of state.nodes) { |
| 192 |
if (node.parentNode !== container) { |
| 193 |
return false; |
| 194 |
} |
| 195 |
} |
| 196 |
return true; |
| 197 |
} |
| 198 |
function render(result, container) { |
| 199 |
const existing = mountState.get(container); |
| 200 |
if (existing && existing.strings === result.strings && mountIntact(existing, container)) { |
| 201 |
applyValues(existing.parts, result.values); |
| 202 |
return; |
| 203 |
} |
| 204 |
const compiled = compile(result.strings); |
| 205 |
const fragment = compiled.template.content.cloneNode(true); |
| 206 |
const parts = compiled.buildParts(fragment); |
| 207 |
const nodes = Array.from(fragment.childNodes); |
| 208 |
while (container.firstChild) { |
| 209 |
container.removeChild(container.firstChild); |
| 210 |
} |
| 211 |
container.appendChild(fragment); |
| 212 |
applyValues(parts, result.values); |
| 213 |
mountState.set(container, { strings: result.strings, parts, nodes }); |
| 214 |
} |
| 215 |
function applyValues(parts, values) { |
| 216 |
for (const part of parts) { |
| 217 |
if (part.kind === "node") { |
| 218 |
updateChildPart(part.child, values[part.valueIndex]); |
| 219 |
} else if (part.kind === "attr") { |
| 220 |
let composed = part.template[0]; |
| 221 |
for (let i = 0; i < part.valueIndices.length; i++) { |
| 222 |
composed += formatText(values[part.valueIndices[i]]); |
| 223 |
composed += part.template[i + 1]; |
| 224 |
} |
| 225 |
if (composed !== part.last) { |
| 226 |
part.last = composed; |
| 227 |
if (composed === "") { |
| 228 |
part.element.removeAttribute(part.name); |
| 229 |
} else { |
| 230 |
part.element.setAttribute(part.name, composed); |
| 231 |
} |
| 232 |
} |
| 233 |
} else if (part.kind === "event") { |
| 234 |
const next = values[part.valueIndex]; |
| 235 |
if (next !== part.current) { |
| 236 |
if (part.current) { |
| 237 |
part.element.removeEventListener(part.name, part.current); |
| 238 |
} |
| 239 |
if (next) { |
| 240 |
part.element.addEventListener(part.name, next); |
| 241 |
} |
| 242 |
part.current = next; |
| 243 |
} |
| 244 |
} else if (part.kind === "prop") { |
| 245 |
const next = values[part.valueIndex]; |
| 246 |
if (next !== part.last) { |
| 247 |
part.last = next; |
| 248 |
part.element[part.name] = next; |
| 249 |
} |
| 250 |
} else if (part.kind === "bool") { |
| 251 |
const next = !!values[part.valueIndex]; |
| 252 |
if (next !== part.last) { |
| 253 |
part.last = next; |
| 254 |
if (next) { |
| 255 |
part.element.setAttribute(part.name, ""); |
| 256 |
} else { |
| 257 |
part.element.removeAttribute(part.name); |
| 258 |
} |
| 259 |
} |
| 260 |
} |
| 261 |
} |
| 262 |
} |
| 263 |
function updateChildPart(child, value) { |
| 264 |
if (value === null || value === void 0 || value === false) { |
| 265 |
if (child.state) { |
| 266 |
disposeChildState(child.state); |
| 267 |
child.state = null; |
| 268 |
} |
| 269 |
return; |
| 270 |
} |
| 271 |
if (Array.isArray(value)) { |
| 272 |
updateArrayChild(child, value); |
| 273 |
return; |
| 274 |
} |
| 275 |
if (isTemplateResult(value)) { |
| 276 |
updateTemplateChild(child, value); |
| 277 |
return; |
| 278 |
} |
| 279 |
if (value instanceof Node) { |
| 280 |
updateNodeChild(child, value); |
| 281 |
return; |
| 282 |
} |
| 283 |
updateTextChild(child, formatText(value)); |
| 284 |
} |
| 285 |
function updateNodeChild(child, node) { |
| 286 |
const old = child.state; |
| 287 |
if (old?.shape === "node" && old.node === node) { |
| 288 |
return; |
| 289 |
} |
| 290 |
if (old) { |
| 291 |
disposeChildState(old); |
| 292 |
} |
| 293 |
insertBeforeAnchor(child, [node]); |
| 294 |
child.state = { shape: "node", node }; |
| 295 |
} |
| 296 |
function updateTextChild(child, text) { |
| 297 |
const old = child.state; |
| 298 |
if (old?.shape === "text") { |
| 299 |
if (old.text !== text) { |
| 300 |
old.node.textContent = text; |
| 301 |
old.text = text; |
| 302 |
} |
| 303 |
return; |
| 304 |
} |
| 305 |
if (old) { |
| 306 |
disposeChildState(old); |
| 307 |
} |
| 308 |
const node = document.createTextNode(text); |
| 309 |
insertBeforeAnchor(child, [node]); |
| 310 |
child.state = { shape: "text", node, text }; |
| 311 |
} |
| 312 |
function updateTemplateChild(child, result) { |
| 313 |
const old = child.state; |
| 314 |
if (old?.shape === "template" && old.strings === result.strings) { |
| 315 |
applyValues(old.parts, result.values); |
| 316 |
return; |
| 317 |
} |
| 318 |
if (old) { |
| 319 |
disposeChildState(old); |
| 320 |
} |
| 321 |
const compiled = compile(result.strings); |
| 322 |
const fragment = compiled.template.content.cloneNode(true); |
| 323 |
const parts = compiled.buildParts(fragment); |
| 324 |
const topNodes = Array.from(fragment.childNodes); |
| 325 |
insertBeforeAnchor(child, [fragment]); |
| 326 |
applyValues(parts, result.values); |
| 327 |
child.state = { |
| 328 |
shape: "template", |
| 329 |
strings: result.strings, |
| 330 |
parts, |
| 331 |
nodes: topNodes |
| 332 |
}; |
| 333 |
} |
| 334 |
function updateArrayChild(child, arr) { |
| 335 |
const old = child.state; |
| 336 |
if (old?.shape === "array" && old.entries.length === arr.length) { |
| 337 |
for (let i = 0; i < arr.length; i++) { |
| 338 |
updateChildPart(old.entries[i], arr[i]); |
| 339 |
} |
| 340 |
return; |
| 341 |
} |
| 342 |
if (old) { |
| 343 |
disposeChildState(old); |
| 344 |
} |
| 345 |
const entries = []; |
| 346 |
for (const v of arr) { |
| 347 |
const entryAnchor = document.createTextNode(""); |
| 348 |
insertBeforeAnchor(child, [entryAnchor]); |
| 349 |
const entry = { anchor: entryAnchor, state: null }; |
| 350 |
updateChildPart(entry, v); |
| 351 |
entries.push(entry); |
| 352 |
} |
| 353 |
child.state = { shape: "array", entries }; |
| 354 |
} |
| 355 |
function insertBeforeAnchor(child, nodes) { |
| 356 |
const parent = child.anchor.parentNode; |
| 357 |
if (!parent) { |
| 358 |
return; |
| 359 |
} |
| 360 |
for (const node of nodes) { |
| 361 |
parent.insertBefore(node, child.anchor); |
| 362 |
} |
| 363 |
} |
| 364 |
function disposeChildState(state) { |
| 365 |
if (state.shape === "text") { |
| 366 |
state.node.remove(); |
| 367 |
return; |
| 368 |
} |
| 369 |
if (state.shape === "template") { |
| 370 |
for (const node of state.nodes) { |
| 371 |
if (node.parentNode) { |
| 372 |
node.parentNode.removeChild(node); |
| 373 |
} |
| 374 |
} |
| 375 |
return; |
| 376 |
} |
| 377 |
if (state.shape === "node") { |
| 378 |
if (state.node.parentNode) { |
| 379 |
state.node.parentNode.removeChild(state.node); |
| 380 |
} |
| 381 |
return; |
| 382 |
} |
| 383 |
for (const entry of state.entries) { |
| 384 |
if (entry.state) { |
| 385 |
disposeChildState(entry.state); |
| 386 |
} |
| 387 |
entry.anchor.remove(); |
| 388 |
} |
| 389 |
} |
| 390 |
function formatText(v) { |
| 391 |
if (v === null || v === void 0 || v === false) { |
| 392 |
return ""; |
| 393 |
} |
| 394 |
return String(v); |
| 395 |
} |
| 396 |
const _Component = class _Component extends HTMLElement { |
| 397 |
constructor() { |
| 398 |
super(); |
| 399 |
this._renderScheduled = false; |
| 400 |
this._propValues = {}; |
| 401 |
const ctor = this.constructor; |
| 402 |
if (ctor.shadow) { |
| 403 |
this.attachShadow({ mode: "open" }); |
| 404 |
this._renderRoot = this.shadowRoot; |
| 405 |
} else { |
| 406 |
this._renderRoot = this; |
| 407 |
} |
| 408 |
this._installPropAccessors(); |
| 409 |
} |
| 410 |
static get observedAttributes() { |
| 411 |
return this.props.map(kebab); |
| 412 |
} |
| 413 |
connectedCallback() { |
| 414 |
this._adoptStyles(); |
| 415 |
this.requestUpdate(); |
| 416 |
} |
| 417 |
attributeChangedCallback(name, oldValue, newValue) { |
| 418 |
if (oldValue === newValue) { |
| 419 |
return; |
| 420 |
} |
| 421 |
const prop = camel(name); |
| 422 |
this._propValues[prop] = newValue; |
| 423 |
this.requestUpdate(); |
| 424 |
} |
| 425 |
/** |
| 426 |
* Declarative class-name setter. Assign an array (or a |
| 427 |
* space-separated string) and the host's `class` attribute is |
| 428 |
* rewritten to match. Intended for programmatic styling — when |
| 429 |
* a plugin has enqueued its own stylesheet and wants to apply |
| 430 |
* one of those classes to a shell component: |
| 431 |
* |
| 432 |
* ```js |
| 433 |
* element.classNames = [ 'my-plugin-brand', 'is-active' ]; |
| 434 |
* // → <wpd-select class="my-plugin-brand is-active"> |
| 435 |
* ``` |
| 436 |
* |
| 437 |
* The plain HTML `class="…"` attribute works just the same and |
| 438 |
* is always preferred when writing markup by hand — this setter |
| 439 |
* exists for the JS-API case where the caller has an array of |
| 440 |
* conditional classes in hand. |
| 441 |
* |
| 442 |
* Getter returns the current `classList` as a plain array for |
| 443 |
* symmetric read/write. |
| 444 |
* |
| 445 |
* @since 0.5.0 |
| 446 |
*/ |
| 447 |
get classNames() { |
| 448 |
return Array.from(this.classList); |
| 449 |
} |
| 450 |
set classNames(next) { |
| 451 |
if (next === null || next === void 0) { |
| 452 |
this.removeAttribute("class"); |
| 453 |
return; |
| 454 |
} |
| 455 |
const list = Array.isArray(next) ? next : String(next).split(/\s+/); |
| 456 |
const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== ""); |
| 457 |
this.className = cleaned.join(" "); |
| 458 |
} |
| 459 |
/** |
| 460 |
* Request a re-render explicitly. Components rarely need this — |
| 461 |
* declare state via props + attribute observers and the render |
| 462 |
* loop picks up changes automatically. |
| 463 |
*/ |
| 464 |
requestUpdate() { |
| 465 |
this._scheduleRender(); |
| 466 |
} |
| 467 |
/** |
| 468 |
* Dispatch a `CustomEvent` with a `detail`. Bubbles + composed |
| 469 |
* by default (matches typical WC UX — events cross shadow |
| 470 |
* boundaries, parents can listen without knowing about internal |
| 471 |
* structure). |
| 472 |
*/ |
| 473 |
emit(name, detail) { |
| 474 |
return this.dispatchEvent( |
| 475 |
new CustomEvent(name, { |
| 476 |
detail, |
| 477 |
bubbles: true, |
| 478 |
composed: true |
| 479 |
}) |
| 480 |
); |
| 481 |
} |
| 482 |
// ------------------------------------------------------------------ |
| 483 |
// Internals |
| 484 |
// ------------------------------------------------------------------ |
| 485 |
/** |
| 486 |
* Wire every `static props` entry to a matched property getter + |
| 487 |
* setter on the element. Setting the property reflects into the |
| 488 |
* attribute (so downstream observers + CSS selectors see it); |
| 489 |
* reading the property falls back to the attribute. |
| 490 |
*/ |
| 491 |
_installPropAccessors() { |
| 492 |
const ctor = this.constructor; |
| 493 |
for (const prop of ctor.props) { |
| 494 |
if (Object.getOwnPropertyDescriptor(this, prop)) { |
| 495 |
continue; |
| 496 |
} |
| 497 |
const attr = kebab(prop); |
| 498 |
Object.defineProperty(this, prop, { |
| 499 |
get: () => { |
| 500 |
if (prop in this._propValues) { |
| 501 |
return this._propValues[prop]; |
| 502 |
} |
| 503 |
return this.getAttribute(attr); |
| 504 |
}, |
| 505 |
set: (value) => { |
| 506 |
let str; |
| 507 |
if (value === null || value === void 0 || value === false) { |
| 508 |
str = null; |
| 509 |
} else if (value === true) { |
| 510 |
str = ""; |
| 511 |
} else { |
| 512 |
str = String(value); |
| 513 |
} |
| 514 |
this._propValues[prop] = str; |
| 515 |
if (str === null) { |
| 516 |
this.removeAttribute(attr); |
| 517 |
} else { |
| 518 |
this.setAttribute(attr, str); |
| 519 |
} |
| 520 |
this.requestUpdate(); |
| 521 |
}, |
| 522 |
enumerable: true, |
| 523 |
configurable: true |
| 524 |
}); |
| 525 |
} |
| 526 |
} |
| 527 |
/** |
| 528 |
* Schedule a render on the next microtask. Multiple property |
| 529 |
* assignments in the same tick collapse into a single render. |
| 530 |
*/ |
| 531 |
_scheduleRender() { |
| 532 |
if (this._renderScheduled || !this.isConnected) { |
| 533 |
return; |
| 534 |
} |
| 535 |
this._renderScheduled = true; |
| 536 |
queueMicrotask(() => { |
| 537 |
this._renderScheduled = false; |
| 538 |
if (!this.isConnected) { |
| 539 |
return; |
| 540 |
} |
| 541 |
render(this.render(), this._renderRoot); |
| 542 |
}); |
| 543 |
} |
| 544 |
/** |
| 545 |
* Mount adoptable stylesheets onto the shadow root (via |
| 546 |
* `adoptedStyleSheets`) or the light DOM (via one `<style>` |
| 547 |
* tag per def). No-op if `static styles` is empty. |
| 548 |
*/ |
| 549 |
_adoptStyles() { |
| 550 |
const ctor = this.constructor; |
| 551 |
if (ctor.styles.length === 0) { |
| 552 |
return; |
| 553 |
} |
| 554 |
if (ctor.shadow && this.shadowRoot) { |
| 555 |
const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null); |
| 556 |
this.shadowRoot.adoptedStyleSheets = sheets; |
| 557 |
if (sheets.length !== ctor.styles.length) { |
| 558 |
for (const s of ctor.styles) { |
| 559 |
if (!s.sheet) { |
| 560 |
const tag = document.createElement("style"); |
| 561 |
tag.textContent = s.cssText; |
| 562 |
this.shadowRoot.appendChild(tag); |
| 563 |
} |
| 564 |
} |
| 565 |
} |
| 566 |
} else { |
| 567 |
this._adoptLightStyles(ctor); |
| 568 |
} |
| 569 |
} |
| 570 |
_adoptLightStyles(ctor) { |
| 571 |
if (_Component._lightStylesAdopted.has(ctor)) { |
| 572 |
return; |
| 573 |
} |
| 574 |
_Component._lightStylesAdopted.add(ctor); |
| 575 |
for (const s of ctor.styles) { |
| 576 |
const tag = document.createElement("style"); |
| 577 |
tag.dataset.wpdUi = this.tagName.toLowerCase(); |
| 578 |
tag.textContent = s.cssText; |
| 579 |
document.head.appendChild(tag); |
| 580 |
} |
| 581 |
} |
| 582 |
}; |
| 583 |
_Component.props = []; |
| 584 |
_Component.styles = []; |
| 585 |
_Component.shadow = true; |
| 586 |
_Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet(); |
| 587 |
let Component = _Component; |
| 588 |
function defineComponent(tag, ctor) { |
| 589 |
if (customElements.get(tag)) { |
| 590 |
return; |
| 591 |
} |
| 592 |
customElements.define(tag, ctor); |
| 593 |
} |
| 594 |
function kebab(s) { |
| 595 |
return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); |
| 596 |
} |
| 597 |
function camel(s) { |
| 598 |
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); |
| 599 |
} |
| 600 |
const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => { |
| 601 |
try { |
| 602 |
const s = new CSSStyleSheet(); |
| 603 |
return typeof s.replaceSync === "function"; |
| 604 |
} catch { |
| 605 |
return false; |
| 606 |
} |
| 607 |
})(); |
| 608 |
function css(strings, ...values) { |
| 609 |
let text = strings[0]; |
| 610 |
for (let i = 1; i < strings.length; i++) { |
| 611 |
const v = values[i - 1]; |
| 612 |
if (typeof v === "string" || typeof v === "number") { |
| 613 |
text += String(v); |
| 614 |
} else if (v && v.__wpdCss) { |
| 615 |
text += v.cssText; |
| 616 |
} else { |
| 617 |
throw new TypeError( |
| 618 |
"[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v |
| 619 |
); |
| 620 |
} |
| 621 |
text += strings[i]; |
| 622 |
} |
| 623 |
if (SUPPORTS_CONSTRUCTABLE_SHEETS) { |
| 624 |
const sheet = new CSSStyleSheet(); |
| 625 |
sheet.replaceSync(text); |
| 626 |
return { __wpdCss: true, sheet, cssText: text }; |
| 627 |
} |
| 628 |
return { __wpdCss: true, sheet: null, cssText: text }; |
| 629 |
} |
| 630 |
function computeAutoId(element) { |
| 631 |
const parts = []; |
| 632 |
const tabs = []; |
| 633 |
let windowId = null; |
| 634 |
let node = element.parentElement; |
| 635 |
while (node) { |
| 636 |
if (node === document.body || node === document.documentElement) { |
| 637 |
break; |
| 638 |
} |
| 639 |
const id = node.id || ""; |
| 640 |
if (id.startsWith("wp-window-")) { |
| 641 |
windowId = id.slice("wp-window-".length); |
| 642 |
break; |
| 643 |
} |
| 644 |
if (node.tagName.toLowerCase() === "wpd-tabpanel") { |
| 645 |
const forValue = node.getAttribute("for"); |
| 646 |
if (forValue) { |
| 647 |
tabs.unshift(forValue); |
| 648 |
} |
| 649 |
} |
| 650 |
node = node.parentElement; |
| 651 |
} |
| 652 |
if (windowId) { |
| 653 |
parts.push(slugify(windowId)); |
| 654 |
} |
| 655 |
for (const tab of tabs) { |
| 656 |
parts.push("tab-" + slugify(tab)); |
| 657 |
} |
| 658 |
const label = element.getAttribute("label"); |
| 659 |
if (label) { |
| 660 |
parts.push(slugify(label)); |
| 661 |
} |
| 662 |
if (parts.length === 0) { |
| 663 |
return "wpd-unnamed"; |
| 664 |
} |
| 665 |
return "wpd-" + parts.filter((p) => p !== "").join("-"); |
| 666 |
} |
| 667 |
function slugify(s) { |
| 668 |
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); |
| 669 |
} |
| 670 |
function ensureAutoId(element) { |
| 671 |
if (element.id) { |
| 672 |
return element.id; |
| 673 |
} |
| 674 |
const id = computeAutoId(element); |
| 675 |
element.id = id; |
| 676 |
return id; |
| 677 |
} |
| 678 |
const styles = css`:host{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var( --desktop-mode-text,#1d2327 );cursor:pointer}label{display:inline-flex;align-items:center;gap:6px;cursor:pointer}input[ type='checkbox' ]{accent-color:var( --wp-admin-theme-color,#2271b1 );cursor:pointer}:host( [ disabled ] ){opacity:0.5;cursor:not-allowed}:host( [ disabled ] ) label,:host( [ disabled ] ) input[ type='checkbox' ]{cursor:not-allowed}`; |
| 679 |
const _WpdCheckboxLabel = class _WpdCheckboxLabel extends Component { |
| 680 |
render() { |
| 681 |
const label = this.label || ""; |
| 682 |
const checked = this.checked !== null; |
| 683 |
const disabled = this.disabled !== null; |
| 684 |
return html` |
| 685 |
<label> |
| 686 |
<input |
| 687 |
type="checkbox" |
| 688 |
?checked=${checked} |
| 689 |
?disabled=${disabled} |
| 690 |
@change=${(e) => this._onChange(e)} |
| 691 |
/> |
| 692 |
<span class="wpd-checkbox-label__text">${label}</span> |
| 693 |
</label> |
| 694 |
`; |
| 695 |
} |
| 696 |
_onChange(e) { |
| 697 |
if (this.disabled !== null) { |
| 698 |
return; |
| 699 |
} |
| 700 |
const next = e.target.checked; |
| 701 |
if (next) { |
| 702 |
this.setAttribute("checked", ""); |
| 703 |
} else { |
| 704 |
this.removeAttribute("checked"); |
| 705 |
} |
| 706 |
this.emit("wpd-checkbox-change", { checked: next }); |
| 707 |
} |
| 708 |
}; |
| 709 |
_WpdCheckboxLabel.props = ["label", "checked", "disabled"]; |
| 710 |
_WpdCheckboxLabel.styles = [styles]; |
| 711 |
_WpdCheckboxLabel.help = { |
| 712 |
title: "Checkbox label", |
| 713 |
summary: "Opinionated label-row variant of <wpd-checkbox>: label text + checkbox in a single aligned row. Use when you want the shipped layout without any layout work.", |
| 714 |
status: "stable", |
| 715 |
since: "0.9.0", |
| 716 |
props: [ |
| 717 |
{ |
| 718 |
name: "label", |
| 719 |
type: "string", |
| 720 |
description: "Visible label text, paired with the checkbox via a native <label>." |
| 721 |
}, |
| 722 |
{ |
| 723 |
name: "checked", |
| 724 |
type: "boolean attribute", |
| 725 |
description: "Reflects and controls the checked state." |
| 726 |
}, |
| 727 |
{ |
| 728 |
name: "disabled", |
| 729 |
type: "boolean attribute", |
| 730 |
description: "When present, the checkbox is not interactive and dimmed." |
| 731 |
} |
| 732 |
], |
| 733 |
events: [ |
| 734 |
{ |
| 735 |
name: "wpd-checkbox-change", |
| 736 |
description: "Fires when the user toggles the checkbox.", |
| 737 |
detail: "{ checked: boolean }" |
| 738 |
} |
| 739 |
], |
| 740 |
cssProps: [ |
| 741 |
{ name: "--desktop-mode-text", description: "Label colour." } |
| 742 |
], |
| 743 |
example: html` |
| 744 |
<wpd-checkbox-label label="Reduce motion" checked></wpd-checkbox-label> |
| 745 |
` |
| 746 |
}; |
| 747 |
let WpdCheckboxLabel = _WpdCheckboxLabel; |
| 748 |
defineComponent("wpd-checkbox-label", WpdCheckboxLabel); |
| 749 |
const textareaStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-textarea__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}textarea{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;box-sizing:border-box;padding:8px 10px;background:var( --desktop-mode-window-bg,#fff );border:1px solid var( --desktop-mode-border,#dcdcde );border-radius:6px;font:inherit;font-size:13px;line-height:1.45;color:var( --desktop-mode-text,#1d2327 );resize:vertical;transition:border-color 0.12s ease,box-shadow 0.12s ease}textarea:hover{border-color:var( --desktop-mode-muted,#8c8f94 )}textarea:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}textarea:disabled{opacity:0.55;cursor:not-allowed;background:rgba( 0,0,0,0.03 )}textarea[ aria-invalid='true' ]{border-color:#d63638}textarea[ aria-invalid='true' ]:focus-visible{box-shadow:0 0 0 1px #d63638}:host( [ auto-grow ] ) textarea{resize:none;overflow:hidden}`; |
| 750 |
const _WpdTextarea = class _WpdTextarea extends Component { |
| 751 |
constructor() { |
| 752 |
super(...arguments); |
| 753 |
this._textareaEl = null; |
| 754 |
} |
| 755 |
connectedCallback() { |
| 756 |
super.connectedCallback(); |
| 757 |
ensureAutoId(this); |
| 758 |
} |
| 759 |
render() { |
| 760 |
const label = this._attr("label") || ""; |
| 761 |
const value = this._attr("value") ?? ""; |
| 762 |
const placeholder = this._attr("placeholder") || ""; |
| 763 |
const disabled = this._boolAttr("disabled"); |
| 764 |
const readonly = this._boolAttr("readonly"); |
| 765 |
const ariaLabel = this._attr("aria-label") || label; |
| 766 |
const name = this._attr("name") || ""; |
| 767 |
const rows = Number(this._attr("rows")) || 3; |
| 768 |
const maxLength = this._attr("maxlength"); |
| 769 |
const minLength = this._attr("minlength"); |
| 770 |
const invalid = this._boolAttr("invalid"); |
| 771 |
const hostId = this.id || "wpd-unnamed"; |
| 772 |
const fieldId = `${hostId}__field`; |
| 773 |
return html` |
| 774 |
${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``} |
| 775 |
<textarea |
| 776 |
id=${fieldId} |
| 777 |
part="textarea" |
| 778 |
.value=${value} |
| 779 |
placeholder=${placeholder} |
| 780 |
?disabled=${disabled} |
| 781 |
?readonly=${readonly} |
| 782 |
rows=${rows} |
| 783 |
maxlength=${maxLength ?? ""} |
| 784 |
minlength=${minLength ?? ""} |
| 785 |
name=${name} |
| 786 |
aria-invalid=${invalid ? "true" : "false"} |
| 787 |
aria-label=${ariaLabel || ""} |
| 788 |
@input=${(e) => this._onInput(e)} |
| 789 |
@change=${(e) => this._onChange(e)} |
| 790 |
@keydown=${(e) => this._onKeyDown(e)} |
| 791 |
></textarea> |
| 792 |
`; |
| 793 |
} |
| 794 |
_attr(name) { |
| 795 |
return this.getAttribute(name); |
| 796 |
} |
| 797 |
_boolAttr(name) { |
| 798 |
return this.getAttribute(name) !== null; |
| 799 |
} |
| 800 |
_onInput(e) { |
| 801 |
const ta = e.target; |
| 802 |
this._textareaEl = ta; |
| 803 |
this.setAttribute("value", ta.value); |
| 804 |
this.emit("wpd-input-change", { value: ta.value }); |
| 805 |
if (this._boolAttr("auto-grow")) { |
| 806 |
this._autosize(ta); |
| 807 |
} |
| 808 |
} |
| 809 |
_onChange(e) { |
| 810 |
const ta = e.target; |
| 811 |
this.emit("wpd-input-commit", { value: ta.value }); |
| 812 |
} |
| 813 |
_onKeyDown(e) { |
| 814 |
if (!this._boolAttr("submit-on-enter")) { |
| 815 |
return; |
| 816 |
} |
| 817 |
if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) { |
| 818 |
e.preventDefault(); |
| 819 |
const ta = e.target; |
| 820 |
this.emit("wpd-submit", { value: ta.value }); |
| 821 |
} |
| 822 |
} |
| 823 |
/** |
| 824 |
* Grow the textarea height to fit content, capped at `max-rows`. |
| 825 |
* Resets to scroll-height each input then clamps; cheap because |
| 826 |
* the browser caches layout. |
| 827 |
*/ |
| 828 |
_autosize(ta) { |
| 829 |
const maxRows = Number(this._attr("max-rows")) || 8; |
| 830 |
const cs = window.getComputedStyle(ta); |
| 831 |
const fontSize = parseFloat(cs.fontSize) || 13; |
| 832 |
const lineHeightRaw = cs.lineHeight; |
| 833 |
const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45; |
| 834 |
const paddingTop = parseFloat(cs.paddingTop) || 0; |
| 835 |
const paddingBottom = parseFloat(cs.paddingBottom) || 0; |
| 836 |
const max = lineHeight * maxRows + paddingTop + paddingBottom; |
| 837 |
ta.style.height = "auto"; |
| 838 |
const next = Math.min(ta.scrollHeight, max); |
| 839 |
ta.style.height = `${next}px`; |
| 840 |
} |
| 841 |
/** Public helper for callers that programmatically set `.value` and want autosize to re-run. */ |
| 842 |
refreshAutosize() { |
| 843 |
if (this._textareaEl && this._boolAttr("auto-grow")) { |
| 844 |
this._autosize(this._textareaEl); |
| 845 |
} |
| 846 |
} |
| 847 |
/** Imperatively focus the underlying textarea. */ |
| 848 |
focusInput() { |
| 849 |
const root = this.shadowRoot ?? this; |
| 850 |
const ta = root.querySelector("textarea"); |
| 851 |
ta?.focus(); |
| 852 |
} |
| 853 |
/** Imperatively clear the value. */ |
| 854 |
clear() { |
| 855 |
this.setAttribute("value", ""); |
| 856 |
const root = this.shadowRoot ?? this; |
| 857 |
const ta = root.querySelector("textarea"); |
| 858 |
if (ta) { |
| 859 |
ta.value = ""; |
| 860 |
if (this._boolAttr("auto-grow")) { |
| 861 |
this._autosize(ta); |
| 862 |
} |
| 863 |
} |
| 864 |
} |
| 865 |
}; |
| 866 |
_WpdTextarea.props = [ |
| 867 |
"label", |
| 868 |
"value", |
| 869 |
"placeholder", |
| 870 |
"disabled", |
| 871 |
"readonly", |
| 872 |
"ariaLabel", |
| 873 |
"name", |
| 874 |
"rows", |
| 875 |
"maxlength", |
| 876 |
"minlength", |
| 877 |
"invalid", |
| 878 |
"autoGrow", |
| 879 |
"maxRows", |
| 880 |
"submitOnEnter" |
| 881 |
]; |
| 882 |
_WpdTextarea.styles = [textareaStyles]; |
| 883 |
_WpdTextarea.help = { |
| 884 |
title: "Textarea", |
| 885 |
summary: "Multi-line text input. Same event shape as wpd-text-field. Optional auto-grow up to max-rows; optional submit-on-enter (Enter sends, Shift+Enter newlines).", |
| 886 |
status: "stable", |
| 887 |
since: "0.6.0", |
| 888 |
props: [ |
| 889 |
{ name: "label", type: "string", description: "Visible label above the textarea." }, |
| 890 |
{ name: "value", type: "string", description: "Current value; reflected two-way." }, |
| 891 |
{ name: "placeholder", type: "string", description: "Native placeholder." }, |
| 892 |
{ name: "disabled", type: "boolean attribute" }, |
| 893 |
{ name: "readonly", type: "boolean attribute" }, |
| 894 |
{ name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." }, |
| 895 |
{ name: "name", type: "string", description: "Forwarded to native textarea for form submission." }, |
| 896 |
{ name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." }, |
| 897 |
{ name: "maxlength", type: "integer (string)" }, |
| 898 |
{ name: "minlength", type: "integer (string)" }, |
| 899 |
{ name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." }, |
| 900 |
{ name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." }, |
| 901 |
{ name: "max-rows", type: "integer (string)", default: "8" }, |
| 902 |
{ |
| 903 |
name: "submit-on-enter", |
| 904 |
type: "boolean attribute", |
| 905 |
description: "Enter fires wpd-submit; Shift+Enter inserts a newline." |
| 906 |
} |
| 907 |
], |
| 908 |
events: [ |
| 909 |
{ name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" }, |
| 910 |
{ name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" }, |
| 911 |
{ |
| 912 |
name: "wpd-submit", |
| 913 |
description: "Fires on Enter (without Shift) when submit-on-enter is set.", |
| 914 |
detail: "{ value: string }" |
| 915 |
} |
| 916 |
], |
| 917 |
example: html` |
| 918 |
<wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea> |
| 919 |
` |
| 920 |
}; |
| 921 |
let WpdTextarea = _WpdTextarea; |
| 922 |
defineComponent("wpd-textarea", WpdTextarea); |
| 923 |
const TEXT_DOMAIN = "desktop-mode"; |
| 924 |
function i18n() { |
| 925 |
return window.wp?.i18n; |
| 926 |
} |
| 927 |
function __(text, domain = TEXT_DOMAIN) { |
| 928 |
return i18n()?.__(text, domain) ?? text; |
| 929 |
} |
| 930 |
const NOTE_COLORS = [ |
| 931 |
"butter", |
| 932 |
"blush", |
| 933 |
"sky", |
| 934 |
"mint", |
| 935 |
"lilac", |
| 936 |
"peach" |
| 937 |
]; |
| 938 |
function normalizeNoteColor(color) { |
| 939 |
return NOTE_COLORS.includes(color) ? color : NOTE_COLORS[0]; |
| 940 |
} |
| 941 |
function nextNoteColor(color) { |
| 942 |
const index = NOTE_COLORS.indexOf( |
| 943 |
normalizeNoteColor(color) |
| 944 |
); |
| 945 |
return NOTE_COLORS[(index + 1) % NOTE_COLORS.length]; |
| 946 |
} |
| 947 |
function hashNoteSeed(text) { |
| 948 |
let hash = 2166136261; |
| 949 |
for (let i = 0; i < text.length; i++) { |
| 950 |
hash ^= text.charCodeAt(i); |
| 951 |
hash = Math.imul(hash, 16777619) >>> 0; |
| 952 |
} |
| 953 |
const seed = hash >>> 1 || 1; |
| 954 |
return seed; |
| 955 |
} |
| 956 |
const PIN_WIDTH = 56; |
| 957 |
const PIN_HEIGHT = 52; |
| 958 |
function pushpinUrl(pluginUrl) { |
| 959 |
return `${pluginUrl.replace(/\/$/, "")}/assets/images/pushpin.svg`; |
| 960 |
} |
| 961 |
function buildPinImage(pluginUrl) { |
| 962 |
const img = document.createElement("img"); |
| 963 |
img.src = pushpinUrl(pluginUrl); |
| 964 |
img.alt = ""; |
| 965 |
img.width = PIN_WIDTH; |
| 966 |
img.height = PIN_HEIGHT; |
| 967 |
img.draggable = false; |
| 968 |
img.className = "desktop-mode-pinned-note__pin-img"; |
| 969 |
return img; |
| 970 |
} |
| 971 |
const NONCE_HEADER = "X-WP-Nonce"; |
| 972 |
function injectRestNonce(input, init) { |
| 973 |
const nonce = readRestNonce(); |
| 974 |
if (!nonce) { |
| 975 |
return init; |
| 976 |
} |
| 977 |
const url = resolveUrl(input); |
| 978 |
if (!url || !isSameOriginRestUrl(url)) { |
| 979 |
return init; |
| 980 |
} |
| 981 |
const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0); |
| 982 |
const headers = new Headers(baseHeaders ?? {}); |
| 983 |
if (headers.has(NONCE_HEADER)) { |
| 984 |
return init; |
| 985 |
} |
| 986 |
headers.set(NONCE_HEADER, nonce); |
| 987 |
return { ...init ?? {}, headers }; |
| 988 |
} |
| 989 |
function readRestNonce() { |
| 990 |
if (typeof window === "undefined") { |
| 991 |
return void 0; |
| 992 |
} |
| 993 |
const cfg = window.desktopModeConfig; |
| 994 |
const value = cfg?.restNonce; |
| 995 |
return typeof value === "string" && value.length > 0 ? value : void 0; |
| 996 |
} |
| 997 |
function resolveUrl(input) { |
| 998 |
try { |
| 999 |
const base = typeof window !== "undefined" && window.location ? window.location.href : void 0; |
| 1000 |
if (typeof input === "string") { |
| 1001 |
return new URL(input, base); |
| 1002 |
} |
| 1003 |
if (input instanceof URL) { |
| 1004 |
return input; |
| 1005 |
} |
| 1006 |
if (typeof Request !== "undefined" && input instanceof Request) { |
| 1007 |
return new URL(input.url, base); |
| 1008 |
} |
| 1009 |
return null; |
| 1010 |
} catch { |
| 1011 |
return null; |
| 1012 |
} |
| 1013 |
} |
| 1014 |
function isSameOriginRestUrl(url) { |
| 1015 |
if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) { |
| 1016 |
return false; |
| 1017 |
} |
| 1018 |
if (url.pathname.includes("/wp-json/")) { |
| 1019 |
return true; |
| 1020 |
} |
| 1021 |
if (url.searchParams.has("rest_route")) { |
| 1022 |
return true; |
| 1023 |
} |
| 1024 |
return false; |
| 1025 |
} |
| 1026 |
function trackedFetch(input, init, opts = {}) { |
| 1027 |
const fn = window.wp?.desktop?.fetch; |
| 1028 |
if (typeof fn === "function") { |
| 1029 |
return fn(input, init, opts); |
| 1030 |
} |
| 1031 |
const finalInit = injectRestNonce(input, init); |
| 1032 |
return fetch(input, finalInit); |
| 1033 |
} |
| 1034 |
let deps = null; |
| 1035 |
function installNotesRestDeps(next) { |
| 1036 |
deps = next; |
| 1037 |
} |
| 1038 |
function ensureDeps() { |
| 1039 |
if (!deps) { |
| 1040 |
throw new Error( |
| 1041 |
"[desktop-mode] notes REST client called before installNotesRestDeps()." |
| 1042 |
); |
| 1043 |
} |
| 1044 |
return deps; |
| 1045 |
} |
| 1046 |
function liveNonce(installed) { |
| 1047 |
const cfg = window.desktopModeConfig; |
| 1048 |
return typeof cfg?.restNonce === "string" && cfg.restNonce ? cfg.restNonce : installed; |
| 1049 |
} |
| 1050 |
class NotesConflictError extends Error { |
| 1051 |
constructor(current) { |
| 1052 |
super("Note was changed by another session."); |
| 1053 |
this.status = 409; |
| 1054 |
this.name = "NotesConflictError"; |
| 1055 |
this.current = current; |
| 1056 |
} |
| 1057 |
} |
| 1058 |
async function call(path, init) { |
| 1059 |
const { baseUrl, nonce } = ensureDeps(); |
| 1060 |
const url = baseUrl; |
| 1061 |
const headers = new Headers(init.headers ?? {}); |
| 1062 |
headers.set("X-WP-Nonce", liveNonce(nonce)); |
| 1063 |
if (init.body && !headers.has("Content-Type")) { |
| 1064 |
headers.set("Content-Type", "application/json"); |
| 1065 |
} |
| 1066 |
const res = await trackedFetch( |
| 1067 |
url, |
| 1068 |
{ ...init, headers, credentials: "same-origin" }, |
| 1069 |
{ source: "desktop-mode/notes" } |
| 1070 |
); |
| 1071 |
const text = await res.text(); |
| 1072 |
let body = null; |
| 1073 |
if (text) { |
| 1074 |
try { |
| 1075 |
body = JSON.parse(text); |
| 1076 |
} catch { |
| 1077 |
body = null; |
| 1078 |
} |
| 1079 |
} |
| 1080 |
if (!res.ok) { |
| 1081 |
if (res.status === 409) { |
| 1082 |
const current = body?.data?.current; |
| 1083 |
throw new NotesConflictError(current ?? null); |
| 1084 |
} |
| 1085 |
const err = body; |
| 1086 |
throw new Error( |
| 1087 |
`[desktop-mode] notes REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim() |
| 1088 |
); |
| 1089 |
} |
| 1090 |
if (null === body) { |
| 1091 |
throw new Error( |
| 1092 |
`[desktop-mode] notes REST ${res.status}: empty or unparseable body.` |
| 1093 |
); |
| 1094 |
} |
| 1095 |
return body; |
| 1096 |
} |
| 1097 |
function createNote(body) { |
| 1098 |
return call("", { |
| 1099 |
method: "POST", |
| 1100 |
body: JSON.stringify(body) |
| 1101 |
}); |
| 1102 |
} |
| 1103 |
const NOTE_DRAFT_PAYLOAD_TYPE = "note-draft"; |
| 1104 |
const NOTE_CREATED_EVENT = "desktop-mode-note-created"; |
| 1105 |
const WIDGET_ID = "desktop-mode/notes"; |
| 1106 |
function readShellConfig() { |
| 1107 |
return window.desktopModeConfig ?? {}; |
| 1108 |
} |
| 1109 |
function getDragManager() { |
| 1110 |
return window.wp?.desktop?.dragManager ?? null; |
| 1111 |
} |
| 1112 |
const mount = (container, ctx) => { |
| 1113 |
let destroyed = false; |
| 1114 |
const config = readShellConfig(); |
| 1115 |
const canCreate = Boolean(config.notesUrl); |
| 1116 |
if (canCreate) { |
| 1117 |
installNotesRestDeps({ |
| 1118 |
baseUrl: config.notesUrl, |
| 1119 |
nonce: config.restNonce ?? "" |
| 1120 |
}); |
| 1121 |
} |
| 1122 |
let color = normalizeNoteColor( |
| 1123 |
ctx.storage.get("color") ?? NOTE_COLORS[0] |
| 1124 |
); |
| 1125 |
let isPublic = ctx.storage.get("public") ?? false; |
| 1126 |
let text = ""; |
| 1127 |
const root = document.createElement("div"); |
| 1128 |
root.className = "dm-notes-pad"; |
| 1129 |
const stack = document.createElement("div"); |
| 1130 |
stack.className = "dm-notes-pad__stack"; |
| 1131 |
const under2 = document.createElement("div"); |
| 1132 |
under2.className = "dm-notes-pad__under dm-notes-pad__under--2"; |
| 1133 |
const under1 = document.createElement("div"); |
| 1134 |
under1.className = "dm-notes-pad__under dm-notes-pad__under--1"; |
| 1135 |
const sheet = document.createElement("div"); |
| 1136 |
sheet.className = "dm-notes-pad__sheet"; |
| 1137 |
const peel = document.createElement("div"); |
| 1138 |
peel.className = "dm-notes-pad__peel"; |
| 1139 |
peel.setAttribute("aria-hidden", "true"); |
| 1140 |
const peelHint = document.createElement("span"); |
| 1141 |
peelHint.className = "dm-notes-pad__peel-hint"; |
| 1142 |
peelHint.textContent = __("Drag to pin", "desktop-mode"); |
| 1143 |
peel.appendChild(peelHint); |
| 1144 |
const editor = document.createElement("wpd-textarea"); |
| 1145 |
editor.className = "dm-notes-pad__editor"; |
| 1146 |
editor.setAttribute("aria-label", __("New note", "desktop-mode")); |
| 1147 |
editor.setAttribute("placeholder", __("Write a note…", "desktop-mode")); |
| 1148 |
editor.setAttribute("rows", "5"); |
| 1149 |
editor.setAttribute("auto-grow", ""); |
| 1150 |
editor.setAttribute("max-rows", "8"); |
| 1151 |
const corner = document.createElement("button"); |
| 1152 |
corner.type = "button"; |
| 1153 |
corner.className = "dm-notes-pad__corner"; |
| 1154 |
sheet.append(peel, editor, corner); |
| 1155 |
stack.append(under2, under1, sheet); |
| 1156 |
const footer = document.createElement("div"); |
| 1157 |
footer.className = "dm-notes-pad__footer"; |
| 1158 |
const swatches = document.createElement("div"); |
| 1159 |
swatches.className = "dm-notes-pad__swatches"; |
| 1160 |
swatches.setAttribute("role", "radiogroup"); |
| 1161 |
swatches.setAttribute("aria-label", __("Paper color", "desktop-mode")); |
| 1162 |
const swatchButtons = /* @__PURE__ */ new Map(); |
| 1163 |
for (const slug of NOTE_COLORS) { |
| 1164 |
const dot = document.createElement("button"); |
| 1165 |
dot.type = "button"; |
| 1166 |
dot.className = "dm-notes-pad__swatch"; |
| 1167 |
dot.dataset.noteColor = slug; |
| 1168 |
dot.setAttribute("role", "radio"); |
| 1169 |
dot.setAttribute("aria-label", slug); |
| 1170 |
dot.addEventListener("click", () => setColor(slug)); |
| 1171 |
swatchButtons.set(slug, dot); |
| 1172 |
swatches.appendChild(dot); |
| 1173 |
} |
| 1174 |
const publicToggle = document.createElement("wpd-checkbox-label"); |
| 1175 |
publicToggle.className = "dm-notes-pad__public"; |
| 1176 |
publicToggle.setAttribute( |
| 1177 |
"label", |
| 1178 |
__("Public — visible to other desktop users", "desktop-mode") |
| 1179 |
); |
| 1180 |
if (isPublic) { |
| 1181 |
publicToggle.setAttribute("checked", ""); |
| 1182 |
} |
| 1183 |
publicToggle.addEventListener("wpd-checkbox-change", (ev) => { |
| 1184 |
isPublic = ev.detail.checked; |
| 1185 |
ctx.storage.set("public", isPublic); |
| 1186 |
}); |
| 1187 |
const pinButton = document.createElement("button"); |
| 1188 |
pinButton.type = "button"; |
| 1189 |
pinButton.className = "dm-notes-pad__pin-btn"; |
| 1190 |
pinButton.textContent = __("Pin to desktop", "desktop-mode"); |
| 1191 |
pinButton.title = __( |
| 1192 |
"Pin the note without dragging (Ctrl+Enter)", |
| 1193 |
"desktop-mode" |
| 1194 |
); |
| 1195 |
footer.append(swatches, publicToggle, pinButton); |
| 1196 |
root.append(stack, footer); |
| 1197 |
container.appendChild(root); |
| 1198 |
function refreshColors() { |
| 1199 |
const next1 = nextNoteColor(color); |
| 1200 |
const next2 = nextNoteColor(next1); |
| 1201 |
sheet.dataset.noteColor = color; |
| 1202 |
stack.dataset.noteColor = color; |
| 1203 |
under1.dataset.noteColor = next1; |
| 1204 |
under2.dataset.noteColor = next2; |
| 1205 |
corner.dataset.noteColor = next1; |
| 1206 |
corner.setAttribute( |
| 1207 |
"aria-label", |
| 1208 |
`${__("Next paper color", "desktop-mode")}: ${next1}` |
| 1209 |
); |
| 1210 |
for (const [slug, dot] of swatchButtons) { |
| 1211 |
dot.setAttribute( |
| 1212 |
"aria-checked", |
| 1213 |
slug === color ? "true" : "false" |
| 1214 |
); |
| 1215 |
dot.classList.toggle("is-selected", slug === color); |
| 1216 |
} |
| 1217 |
} |
| 1218 |
function setColor(slug) { |
| 1219 |
color = normalizeNoteColor(slug); |
| 1220 |
ctx.storage.set("color", color); |
| 1221 |
refreshColors(); |
| 1222 |
} |
| 1223 |
const onCornerClick = () => setColor(nextNoteColor(color)); |
| 1224 |
corner.addEventListener("click", onCornerClick); |
| 1225 |
refreshColors(); |
| 1226 |
const onInput = (ev) => { |
| 1227 |
text = ev.detail.value; |
| 1228 |
}; |
| 1229 |
editor.addEventListener("wpd-input-change", onInput); |
| 1230 |
const onEditorKeydown = (ev) => { |
| 1231 |
const kev = ev; |
| 1232 |
if (kev.key === "Enter" && (kev.ctrlKey || kev.metaKey)) { |
| 1233 |
kev.preventDefault(); |
| 1234 |
void pinWithoutDrag(); |
| 1235 |
} |
| 1236 |
kev.stopPropagation(); |
| 1237 |
}; |
| 1238 |
["keydown", "keypress", "keyup"].forEach( |
| 1239 |
(name) => editor.addEventListener(name, (ev) => { |
| 1240 |
if (name === "keydown") { |
| 1241 |
onEditorKeydown(ev); |
| 1242 |
} else { |
| 1243 |
ev.stopPropagation(); |
| 1244 |
} |
| 1245 |
}) |
| 1246 |
); |
| 1247 |
const clearDraft = () => { |
| 1248 |
text = ""; |
| 1249 |
editor.setAttribute("value", ""); |
| 1250 |
}; |
| 1251 |
const shakeSheet = () => { |
| 1252 |
if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { |
| 1253 |
return; |
| 1254 |
} |
| 1255 |
sheet.animate?.( |
| 1256 |
[ |
| 1257 |
{ transform: "translateX(0)" }, |
| 1258 |
{ transform: "translateX(-4px)" }, |
| 1259 |
{ transform: "translateX(4px)" }, |
| 1260 |
{ transform: "translateX(0)" } |
| 1261 |
], |
| 1262 |
{ duration: 200, easing: "ease-out" } |
| 1263 |
); |
| 1264 |
}; |
| 1265 |
const buildDraftGhost = () => { |
| 1266 |
const width = 208; |
| 1267 |
const ghostRoot = document.createElement("div"); |
| 1268 |
ghostRoot.className = "desktop-mode-pinned-note-ghost"; |
| 1269 |
ghostRoot.dataset.noteColor = color; |
| 1270 |
ghostRoot.style.width = `${width}px`; |
| 1271 |
const swing = document.createElement("div"); |
| 1272 |
swing.className = "desktop-mode-pinned-note-ghost__swing"; |
| 1273 |
swing.dataset.noteColor = color; |
| 1274 |
const tipX = width / 2; |
| 1275 |
const tipY = 10; |
| 1276 |
swing.style.transformOrigin = `${tipX}px ${tipY}px`; |
| 1277 |
const pin = document.createElement("span"); |
| 1278 |
pin.className = "desktop-mode-pinned-note__pin"; |
| 1279 |
pin.style.setProperty("--dm-pin-dx", "0px"); |
| 1280 |
pin.style.setProperty("--dm-pin-rot", "0deg"); |
| 1281 |
pin.appendChild(buildPinImage(ctx.pluginUrl)); |
| 1282 |
const paper = document.createElement("div"); |
| 1283 |
paper.className = "desktop-mode-pinned-note__paper desktop-mode-pinned-note-ghost__paper"; |
| 1284 |
const body = document.createElement("div"); |
| 1285 |
body.className = "desktop-mode-pinned-note__body"; |
| 1286 |
body.textContent = text; |
| 1287 |
paper.appendChild(body); |
| 1288 |
swing.append(pin, paper); |
| 1289 |
ghostRoot.appendChild(swing); |
| 1290 |
return { root: ghostRoot, tipX, tipY }; |
| 1291 |
}; |
| 1292 |
const onSheetPointerDown = (ev) => { |
| 1293 |
if (destroyed || !canCreate) { |
| 1294 |
return; |
| 1295 |
} |
| 1296 |
const target = ev.target; |
| 1297 |
if (target?.closest("wpd-textarea, .dm-notes-pad__corner")) { |
| 1298 |
return; |
| 1299 |
} |
| 1300 |
if (!text.trim()) { |
| 1301 |
shakeSheet(); |
| 1302 |
return; |
| 1303 |
} |
| 1304 |
const dragManager = getDragManager(); |
| 1305 |
if (!dragManager) { |
| 1306 |
return; |
| 1307 |
} |
| 1308 |
ev.preventDefault(); |
| 1309 |
sheet.ownerDocument.defaultView?.getSelection()?.removeAllRanges(); |
| 1310 |
const ghost = buildDraftGhost(); |
| 1311 |
const data = { |
| 1312 |
text, |
| 1313 |
color, |
| 1314 |
isPublic |
| 1315 |
}; |
| 1316 |
dragManager.start({ |
| 1317 |
payload: { |
| 1318 |
type: NOTE_DRAFT_PAYLOAD_TYPE, |
| 1319 |
source: sheet, |
| 1320 |
data, |
| 1321 |
ghost: { |
| 1322 |
element: ghost.root, |
| 1323 |
offsetX: ghost.tipX, |
| 1324 |
offsetY: ghost.tipY, |
| 1325 |
hint: { |
| 1326 |
neutral: __("Drop on the desktop to pin", "desktop-mode"), |
| 1327 |
accept: __("Pin here", "desktop-mode"), |
| 1328 |
reject: __("Can’t pin here", "desktop-mode") |
| 1329 |
} |
| 1330 |
} |
| 1331 |
}, |
| 1332 |
origin: ev, |
| 1333 |
onClickOnly: () => editor.focusInput?.(), |
| 1334 |
onCommit: () => { |
| 1335 |
clearDraft(); |
| 1336 |
playTearOffPromotion(); |
| 1337 |
} |
| 1338 |
// onCancel: the sheet reappears untouched (the manager |
| 1339 |
// removes its source-dragging class) — the draft survives. |
| 1340 |
}); |
| 1341 |
}; |
| 1342 |
sheet.addEventListener("pointerdown", onSheetPointerDown); |
| 1343 |
const playTearOffPromotion = () => { |
| 1344 |
if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { |
| 1345 |
return; |
| 1346 |
} |
| 1347 |
sheet.animate?.( |
| 1348 |
[ |
| 1349 |
{ |
| 1350 |
transform: "translate(3px, 4px) rotate(1.1deg)", |
| 1351 |
opacity: 0.9 |
| 1352 |
}, |
| 1353 |
{ |
| 1354 |
transform: "translate(-1px, -2px) rotate(-0.4deg)", |
| 1355 |
offset: 0.7 |
| 1356 |
}, |
| 1357 |
{ transform: "translate(0, 0) rotate(0deg)", opacity: 1 } |
| 1358 |
], |
| 1359 |
{ duration: 260, easing: "cubic-bezier(0.2, 0.7, 0.2, 1)" } |
| 1360 |
); |
| 1361 |
}; |
| 1362 |
async function pinWithoutDrag() { |
| 1363 |
if (destroyed || !canCreate) { |
| 1364 |
return; |
| 1365 |
} |
| 1366 |
if (!text.trim()) { |
| 1367 |
shakeSheet(); |
| 1368 |
editor.focusInput?.(); |
| 1369 |
return; |
| 1370 |
} |
| 1371 |
pinButton.disabled = true; |
| 1372 |
try { |
| 1373 |
const slot = Math.floor(Date.now() / 1e3) % 5; |
| 1374 |
const note = await createNote({ |
| 1375 |
text, |
| 1376 |
color, |
| 1377 |
x: 0.55 + slot * 0.04, |
| 1378 |
y: 0.12 + slot * 0.05, |
| 1379 |
public: isPublic, |
| 1380 |
seed: hashNoteSeed(text) |
| 1381 |
}); |
| 1382 |
if (destroyed) { |
| 1383 |
return; |
| 1384 |
} |
| 1385 |
clearDraft(); |
| 1386 |
playTearOffPromotion(); |
| 1387 |
document.dispatchEvent( |
| 1388 |
new CustomEvent(NOTE_CREATED_EVENT, { detail: { note } }) |
| 1389 |
); |
| 1390 |
} catch (err) { |
| 1391 |
console.error("[desktop-mode] note pad: create failed:", err); |
| 1392 |
shakeSheet(); |
| 1393 |
const toast = window.wp?.desktop?.showToast; |
| 1394 |
toast?.({ |
| 1395 |
message: __( |
| 1396 |
"Could not pin the note. Please try again.", |
| 1397 |
"desktop-mode" |
| 1398 |
), |
| 1399 |
duration: 5e3 |
| 1400 |
}); |
| 1401 |
} finally { |
| 1402 |
pinButton.disabled = false; |
| 1403 |
} |
| 1404 |
} |
| 1405 |
const onPinButton = () => { |
| 1406 |
void pinWithoutDrag(); |
| 1407 |
}; |
| 1408 |
pinButton.addEventListener("click", onPinButton); |
| 1409 |
if (!canCreate) { |
| 1410 |
root.classList.add("dm-notes-pad--unavailable"); |
| 1411 |
editor.setAttribute("disabled", ""); |
| 1412 |
pinButton.disabled = true; |
| 1413 |
} |
| 1414 |
return () => { |
| 1415 |
destroyed = true; |
| 1416 |
sheet.removeEventListener("pointerdown", onSheetPointerDown); |
| 1417 |
corner.removeEventListener("click", onCornerClick); |
| 1418 |
pinButton.removeEventListener("click", onPinButton); |
| 1419 |
}; |
| 1420 |
}; |
| 1421 |
const w = window; |
| 1422 |
w.desktopModeWidgets = w.desktopModeWidgets ?? {}; |
| 1423 |
w.desktopModeWidgets[WIDGET_ID] = mount; |
| 1424 |
})(); |
| 1425 |
|