@localnerve/editable-object 0.5.0-rc.1 → 0.5.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/editable-object.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
8
|
A native web component for an editable object that allows a user to edit it's values, add or remove key/value pairs. JSON values only.
|
|
9
|
+
The component is [Trusted Types](#trusted-types) aware so it works under a strict CSP (`require-trusted-types-for 'script'`).
|
|
9
10
|
Non-browser module exports build helpers (for building CSP rules, etc).
|
|
10
11
|
|
|
11
12
|
_A convenient, **no-dependency** drop-in 'todo' app component to test/round-trip data updates and mutations on the front end._
|
|
@@ -18,6 +19,7 @@ _A convenient, **no-dependency** drop-in 'todo' app component to test/round-trip
|
|
|
18
19
|
* [Properties and Methods](#javascript-public-properties-and-methods)
|
|
19
20
|
* [CSS Variables](#overridable-css-variables)
|
|
20
21
|
* [Usage Examples](#usage-example)
|
|
22
|
+
* [Trusted Types](#trusted-types)
|
|
21
23
|
* [Non-browser exports](#nonbrowser-exports)
|
|
22
24
|
|
|
23
25
|
## Events
|
|
@@ -117,10 +119,37 @@ See [The test references](https://github.com/localnerve/editable-object/blob/mas
|
|
|
117
119
|
* Disable edit [example](https://github.com/localnerve/editable-object/blob/master/test/fixtures/disable-edit.html)
|
|
118
120
|
* Validation handlers [example](https://github.com/localnerve/editable-object/blob/master/test/fixtures/handlers.html)
|
|
119
121
|
|
|
122
|
+
## Trusted Types
|
|
123
|
+
|
|
124
|
+
This component is written to work both **with** and **without** Trusted Types enforcement. When a page's CSP includes `require-trusted-types-for 'script'`, every DOM injection sink (`innerHTML`, etc.) must receive a value produced by an allowlisted policy — plain strings are rejected.
|
|
125
|
+
|
|
126
|
+
The component handles this on its own:
|
|
127
|
+
|
|
128
|
+
* Its static shadow-DOM template and the per-property markup it renders are handed to sinks through its **own named policy** (`editable-object`).
|
|
129
|
+
* Any user-influenced values (property keys, values) are escaped with `escapeHtml()` before being composed into that markup.
|
|
130
|
+
* In browsers without Trusted Types — or when the page does not enforce a CSP — every helper degrades to a plain-string passthrough, so the component works unchanged in dev and older browsers.
|
|
131
|
+
|
|
132
|
+
The required helpers (`escapeHtml`, `trustedHtml`) are gathered from [`@localnerve/web-component-build`](https://github.com/localnerve/web-component-build#readme) **at build time** and inlined into the shipped bundle. That is why this package has **no runtime dependency** on web-component-build — it is a build-time (dev) dependency only.
|
|
133
|
+
|
|
134
|
+
### CSP requirement
|
|
135
|
+
|
|
136
|
+
Your site's CSP `trusted-types` directive must allowlist the component's policy name:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Content-Security-Policy: ... require-trusted-types-for 'script'; trusted-types default editable-object;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The policy name is exported as `POLICY_NAME` (see [Non-browser Exports](#nonbrowser-exports)) so build tooling can reference it. A build step can also compute the full allowlist automatically from your sources — see [`@localnerve/trusted-types-rules`](https://github.com/localnerve/trusted-types-rules#readme).
|
|
143
|
+
|
|
120
144
|
## Non-browser Exports
|
|
121
145
|
|
|
122
146
|
The non-browser version of the module exports methods to help with builds.
|
|
123
147
|
|
|
148
|
+
### POLICY_NAME {String}
|
|
149
|
+
|
|
150
|
+
The Trusted Types policy name this component registers: `'editable-object'`.
|
|
151
|
+
List it in your CSP `trusted-types` directive when enforcing `require-trusted-types-for 'script'` (see [Trusted Types](#trusted-types)).
|
|
152
|
+
|
|
124
153
|
### {Promise} getEditableObjectCssText()
|
|
125
154
|
|
|
126
155
|
Asynchronously gets the raw shadow css text.
|
package/dist/editable-object.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! editable-object@0.5.0-rc.
|
|
2
|
-
const e=new Map;function t(e){return null==e?"":String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function o(t,o){const r=function(t,o={createHTML:e=>String(e),createScriptURL:e=>String(e)}){return"undefined"!=typeof window&&"trustedTypes"in window?(e.has(t)||e.set(t,trustedTypes.createPolicy(t,o)),e.get(t)):null}(t);return r?r.createHTML(o):o}const r="editable-object";class i extends HTMLElement{#e=null;#t=!1;#o=()=>!0;#r=()=>!0;#i=()=>!0;#s=[];#n=[];#l=[];static#a=["object","add-property-placeholder","disable-edit"];static#d={object:{},"add-property-placeholder":"Add new property in key:value format","disabled-edit":!1};static get observedAttributes(){return this.#a}constructor(){super(),this.attachShadow({mode:"open",delegatesFocus:!0})}#c(e){if(this.hasAttribute(e)){const t=this.getAttribute(e);return/^\s*(?:true|false)\s*$/i.test(t)?"false"!==t:t}return i.#d[e]}#p(e){if("string"==typeof e||"number"==typeof e||"boolean"==typeof e||null===e)return e;if(void 0===e||"function"==typeof e||"symbol"==typeof e)return null;if("bigint"==typeof e)return`${e}n`;"[object RegExp]"===Object.prototype.toString.call(e)&&(e={__pattern:e.source,flags:e.flags});let t=JSON.stringify(e);return"{"===t[0]&&(t=t.replaceAll('"',"'")),t}#h(e,t=null){const o=e.trim();let r=parseFloat(o);if(r)return r;if(/\d+n$/.test(o))return BigInt(o.slice(0,-1));if("false"===o.toLowerCase())return!1;if("true"===o.toLowerCase())return!0;if("null"===o.toLowerCase())return null;let i,s=!1;try{let e=o;"{"===e[0]&&(s=!0,e=e.replaceAll("'",'"'));const t=JSON.parse(e);i=Object.keys(t).includes("__pattern")?new RegExp(t.__pattern,t.flags):t}catch{if(s&&t)throw t.classList.add("error"),new Error("Bad object input");i=o}return i}#u(e,o){const r=t(e);return`\n <div class="property-wrapper">\n <label for="eo-${r}-value">${r}</label>\n <input name="${r}" readonly="true" id="eo-${r}-value" type="text" value="${t(o)}" />\n </div>\n <div class="toolbar">\n <button class="editable-object-up-property icon" title="Move up">\n <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">\n <path d="m5 9 1.41 1.41L11 5.83V22h2V5.83l4.59 4.59L19 9l-7-7-7 7z"></path>\n </svg>\n </button>\n <button class="editable-object-down-property icon" title="Move down">\n <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">\n <path d="m19 15-1.41-1.41L13 18.17V2h-2v16.17l-4.59-4.59L5 15l7 7 7-7z"></path>\n </svg>\n </button>\n <button class="editable-object-remove-property icon" title="Remove">\n <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">\n <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path>\n </svg>\n </button>\n </div>\n `}#b(){const e=this.shadowRoot.querySelectorAll(".editable-object-up-property"),t=this.shadowRoot.querySelectorAll(".editable-object-down-property"),o=e.length;for(let r=0;r<o;r++)e[r].style.visibility=0==r?"hidden":"visible",t[r].style.visibility=r==o-1?"hidden":"visible";const r=this.#t?"add":"remove";this.shadowRoot.querySelectorAll(".editable-object-remove-property").forEach(e=>e.classList[r]("hide"))}#y(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}#v(e){for(;"LI"!==e.tagName;)e=e.parentNode;return e}#f(e){this.#w();const t=this.#v(e.target);t.classList.toggle("selected",!0),[...t.querySelectorAll("button")].forEach(e=>{e.tabIndex=0}),t.querySelector("input").focus()}#g(e){return new CustomEvent("change",{bubbles:!0,cancelable:!1,composed:!0,detail:e})}#m(e){"INPUT"!==e.target.nodeName||e.target.classList.contains("error")||"Enter"!==e.key&&" "!==e.key||e.target.click()}#x(e){if(this._editing)return;this._editing=!0;const t=this.#v(e.target).querySelector(".property-wrapper > input");t.classList.remove("error"),t.readOnly=!1,t._value=t.value;const o=this.#j.bind(this),r=this.#j.bind(this);t.addEventListener("blur",o,!1),this.#l.push({host:t,type:"blur",listener:o}),t.addEventListener("keypress",r,!1),this.#l.push({host:t,type:"keypress",listener:r}),t.focus()}#j(e){if(this._editing&&(e instanceof KeyboardEvent&&"Enter"===e.key||!(e instanceof KeyboardEvent))){e.preventDefault();const t=this.#v(e.target),o=t.querySelector(".property-wrapper > label").innerText,r=t.querySelector(".property-wrapper > input"),i=r._value,s=r.value;let n;this._editing=!1,r.readOnly=!0;let l=!1;try{n=this.#h(s,r)}catch{l=!0}this.#l.forEach(e=>{e.host.removeEventListener(e.type,e.listener)}),this.#l.length=0,l?r.classList.add("error"):this.#o(o,n)?(this.#e[o]=n,this.dispatchEvent(this.#g({action:"edit",key:o,previous:this.#h(i),new:n}))):r.classList.add("error")}}#E(e){const t=e.previousElementSibling;return!!t&&(e.parentNode.insertBefore(e,t),this.#b(),e.querySelector(".editable-object-up-property").focus(),!0)}#L(e){const t=e.nextElementSibling;return!!t&&(e.parentNode.insertBefore(t,e),this.#b(),e.querySelector(".editable-object-down-property").focus(),!0)}#k(e){e.remove(),this.#b()}#S(e){e.stopPropagation();const t=this.#v(e.target),o=t.querySelector(".property-wrapper > label").innerText.trim(),r=t.querySelector(".property-wrapper > input"),i=r.value.trim(),s=this.#h(i);this.#r(o,s)?(this.#k(t),delete this.#e[o],this.dispatchEvent(this.#g({action:"remove",key:o,previous:s,new:null}))):r.classList.add("error")}#q(e){e.stopPropagation();const t=this.#v(e.target);this.#E(t)}#A(e){e.stopPropagation();const t=this.#v(e.target);this.#L(t)}#R(){let e,t=0;return function(o){const r=(new Date).getTime(),i=r-t;i<500&&i>0?(o.preventDefault(),this.#x(o)):e=setTimeout(()=>{clearTimeout(e)},500),t=r}.bind(this)}#P(e){const t=this.#f.bind(this);e.forEach(e=>{e.addEventListener("click",t,!1),this.#n.push({host:e,type:"click",listener:t})})}#_(){const e=this.shadowRoot.querySelectorAll(".object-properties .property-wrapper"),t={remove:this.shadowRoot.querySelectorAll('.object-properties button[title="Remove"]')};this.#M(e,t)}#M(e,t){const o=this.#y(),r=this.#R(),i=this.#x.bind(this),s=this.#S.bind(this);if(this.#t){const e=["dblclick","touchend"],t=[];this.#n.filter((o,r)=>{let i=e.includes(o.type);return i?t.push(r):"remove"===o.host.title.toLowerCase()&&(i=!0,t.push(r)),i}).forEach(e=>{e.host.removeEventListener(e.type,e.listener)});let o=0;for(const e of t)this.#n.splice(e-o++,1)}else this._editing=!1,e.forEach(e=>{e.addEventListener("dblclick",i,!1),this.#n.push({host:e,type:"dblclick",listener:i}),o&&(e.addEventListener("touchend",r),this.#n.push({host:e,type:"touchend",listener:r}))}),t.remove.forEach(e=>{e.addEventListener("click",s,!1),this.#n.push({host:e,type:"click",listener:s})})}#O(e,t){const o=this.#m.bind(this),r=this.#q.bind(this),i=this.#A.bind(this);e.forEach(e=>{e.addEventListener("keypress",o,!1),this.#n.push({host:e,type:"keypress",listener:o})}),t.up.forEach(e=>{e.addEventListener("click",r,!1),this.#n.push({host:e,type:"click",listener:r})}),t.down.forEach(e=>{e.addEventListener("click",i,!1),this.#n.push({host:e,type:"click",listener:i})}),this.#M(e,t)}#T(e){e.composedPath().includes(this)||this.shadowRoot.querySelector(".editable-object").classList.add("defocused")}#C(){this.shadowRoot.querySelector(".editable-object").classList.remove("defocused")}#H(e){return e in this.#e}#w(){this.shadowRoot.querySelector(".add-new-object-property-input").classList.toggle("error",!1),[...this.shadowRoot.querySelectorAll("li")].forEach(e=>{const t=e.querySelector(".property-wrapper > input");t.classList.contains("error")&&t._value&&(t.value=t._value),t.classList.toggle("error",!1),e.classList.toggle("selected",!1),[...e.querySelectorAll("button")].forEach(e=>{e.tabIndex=-1})})}#N(e){if(e instanceof KeyboardEvent&&"Enter"===e.key||!(e instanceof KeyboardEvent)){const e=this.shadowRoot.querySelector(".add-new-object-property-input"),t=e.value.trim();if(""!==t){const o=/^\s*(?<property>[^\s:]+)\s*:\s*(?<value>[^$]+)$/,r=t.match(o)?.groups,[i,s]=r?Object.values(r):["",""],n=i.trim(),l=s.trim(),a=()=>{e.classList.add("error"),e.focus()};if(!n||!l||this.#H(n))return void a();let d,c=!1;try{d=this.#h(l,e)}catch{c=!0}if(c)a();else if(this.#i(n,d)){const t={[n]:l};this.mergeObject(t),this.dispatchEvent(this.#g({action:"add",key:n,previous:null,new:d})),this.shadowRoot.querySelector(".object-properties").lastChild.click(),e.value=""}else a()}}}#B(){const{shadowRoot:e}=this,t=e.querySelector(".new-object-property"),o=e.querySelector(".add-new-object-property-input"),r=e.querySelector(".editable-object-add-property");if(this.#t){const e=[t,o,r],i=[];this.#s.filter((t,o)=>{const r=e.includes(t.host);return r&&i.push(o),r}).forEach(e=>{e.host.removeEventListener(e.type,e.listener)});let s=0;for(const e of i)this.#s.splice(e-s++,1);t.classList.add("hide")}else{const e=this.getAttribute("add-property-placeholder");this.addPropertyPlaceholder=e;const i=this.#w.bind(this);t.addEventListener("click",i,!0),this.#s.push({host:t,type:"click",listener:i});const s=this.#N.bind(this);o.addEventListener("keypress",s,!1),this.#s.push({host:o,type:"keypress",listener:s}),r.addEventListener("click",s,!1),this.#s.push({host:r,type:"click",listener:s}),t.classList.remove("hide")}}get object(){return this.#e}set object(e){if(!e)return;this.#e&&Object.keys(this.#e).length>0&&[this.#n,this.#l].forEach(e=>{e.forEach(e=>{e.host.removeEventListener(e.type,e.listener)})});const t=this.shadowRoot.querySelector("#loading"),i=this.shadowRoot.querySelector(".object-properties");i.innerHTML=o(r,"");const s=[],n=[],l={up:[],down:[],remove:[]};for(const[t,a]of Object.entries(e)){const e=document.createElement("li");e.innerHTML=o(r,this.#u(t,this.#p(a))),i.appendChild(e),s.push(e),n.push(e.querySelector(".property-wrapper")),l.up.push(e.querySelector(".editable-object-up-property")),l.down.push(e.querySelector(".editable-object-down-property")),l.remove.push(e.querySelector(".editable-object-remove-property"))}this.#P(s),this.#O(n,l),this.#b(),t.classList.add("hide"),this.#e=e}get addPropertyPlaceholder(){return this.#c("add-property-placeholder")}set addPropertyPlaceholder(e){const t="add-property-placeholder",o=this.shadowRoot.querySelector(".add-new-object-property-input");e?(this.setAttribute(t,e),o.placeholder=e):(this.removeAttribute(t),o.placeholder=i.#d[t])}set disableEdit(e){const t="disable-edit";e?(this.setAttribute(t,!0),this.#t=!0):(this.setAttribute(t,!1),this.#t=!1),this.#B(),this.#_(),this.#b()}get disableEdit(){return this.#c("disable-edit")}set onEdit(e){this.#o="function"==typeof e?e:()=>!0}get onEdit(){return this.#o}set onAdd(e){this.#i="function"==typeof e?e:()=>!0}get onAdd(){return this.#i}set onRemove(e){this.#r="function"==typeof e?e:()=>!0}get onRemove(){return this.#r}mergeObject(e){this.object={...this.#e,...e}}connectedCallback(){const{shadowRoot:e}=this;e.innerHTML=o(r,'<style>:host{--eo-min-width:300px;--eo-bg-color:#fafafa;--eo-border-radius:4px;--eo-border-focused-color:#444;--eo-border-defocused-color:#aaa;--eo-item-selected-bg-color:#999;--eo-item-selected-color:#222;--eo-item-selected-border-radius:4px;--eo-item-hover-border-width:1px;--eo-item-hover-border-color:#ddd;--eo-item-hover-border-radius:4px;--eo-icon-color:#444;--eo-add-new-icon-color:#444;--eo-input-focus-outline-color:#26b;--eo-input-focus-outline-width:1px;--eo-input-focus-outline-style:auto;--eo-input-border-color:#bbb;--eo-input-border-radius:4px;--eo-input-bg-color:#444;--eo-input-color:#eee;--eo-input-font-family:sans-serif;--eo-input-placeholder-color:#aaa}:host(.disabled){pointer-events:none}.editable-object{background:var(--eo-bg-color);border-radius:var(--eo-border-radius);min-width:var(--eo-min-width);display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}@media (min-width:360px){.editable-object{padding:0 .5rem}}@media (min-width:464px){.editable-object{border:1px solid var(--eo-border-focused-color);padding:1rem}.editable-object.defocused{border:1px solid var(--eo-border-defocused-color)}}ul{padding:0;margin:0;width:100%}li{line-height:2;list-style:none;cursor:default;padding:.5rem}input{padding:6px 8px;border-radius:var(--eo-input-border-radius);line-height:1.5;border:1px solid var(--eo-input-border-color);color:var(--eo-input-color);background:var(--eo-input-bg-color);font-family:var(--eo-input-font-family)}input:focus-visible{outline:var(--eo-input-focus-outline-color) var(--eo-input-focus-outline-style) var(--eo-input-focus-outline-width)}input::placeholder{color:var(--eo-input-placeholder-color)}.editable-object:not(.defocused) li.selected{background:var(--eo-item-selected-bg-color);color:var(--eo-item-selected-color);border-radius:var(--eo-item-selected-border-radius)}div>div,li{display:flex;align-items:center;justify-content:space-around}.editable-object:not(.defocused,.touch) li:hover{border:var(--eo-item-hover-border-width) solid var(--eo-item-hover-border-color);border-radius:var(--eo-item-hover-border-radius)}.property-wrapper{display:flex;flex-flow:row wrap;flex-grow:1;align-items:baseline;min-width:10em;touch-action:manipulation}.property-wrapper label{flex:1 1 100%;min-width:8em;max-width:100%}.property-wrapper input{flex:1;min-width:10em}.editable-object.defocused input{opacity:.7}@media (min-width:41.69em){.property-wrapper label{flex-basis:auto;max-width:45%;min-width:15em;padding-right:.5rem}.property-wrapper input{flex:1 1;min-width:16em}}.toolbar{display:flex;gap:1.5rem;padding-left:1.5rem}.editable-object.defocused .toolbar,li:not(.selected) .toolbar{opacity:0;pointer-events:none}.toolbar button{position:relative;fill:var(--eo-icon-color)}.editable-object-add-property.icon{fill:var(--eo-add-new-icon-color)}.toolbar button:hover::before{content:"";position:absolute;width:24px;height:24px;background:rgb(0 0 0 / 10%);left:-4px;top:-4px;border-radius:50%}.icon{background-color:transparent;border:none;cursor:pointer;font-size:0;fill:var(--eo-icon-color);padding:0}.icon svg{width:1rem;height:1rem}.new-object-property{display:flex;flex-flow:row wrap;line-height:2;margin-top:2rem;width:100%}label[for=new-property]{flex:1 0 100%}.add-new-object-property-input{min-width:18em;flex:1}.add-new-object-property-input.error,.property-wrapper input.error{outline:red auto 1px}.hide{display:none}</style><div class="editable-object defocused"><slot id=loading name=loading></slot><ul class=object-properties></ul><div class=new-object-property><label for=new-property>New Property and Value</label> <input id=new-property name=new-property class=add-new-object-property-input type=text placeholder="Add new property in key:value format"><div class=toolbar><button class="editable-object-add-property icon" title=Add><svg version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink width=24 height=24 viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg></button></div></div></div>');const t=this.getAttribute("object");this.object=JSON.parse(t);const i=this.getAttribute("disable-edit");this.#t="true"===i?.toLowerCase();const s=e.querySelector(".editable-object"),n=e.querySelector("#loading"),l=this.#y(),a=this.#T.bind(this),d=this.#C.bind(this);this.object&&n.classList.add("hide"),l&&s.classList.add("touch"),document.addEventListener("click",a,!1),this.#s.push({host:document,type:"click",listener:a}),s.addEventListener("click",d,!0),this.#s.push({host:s,type:"click",listener:d}),this.#B()}disconnectedCallback(){[this.#s,this.#n,this.#l].forEach(e=>{e.forEach(e=>{e.host.removeEventListener(e.type,e.listener)})})}attributeChangedCallback(e,t,o){o!==t&&(this[e]=this.getAttribute(e))}}customElements.define("editable-object",i);
|
|
1
|
+
/*! editable-object@0.5.0-rc.3, Copyright (c) 2026 Alex Grant <alex@localnerve.com> (https://www.localnerve.com), LocalNerve LLC, BSD-3-Clause */
|
|
2
|
+
const e=new Map;function t(e){return null==e?"":String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/`/g,"`")}function o(t,o){const r=function(t,o={createHTML:e=>String(e),createScriptURL:e=>String(e)}){return"undefined"!=typeof window&&"trustedTypes"in window?(e.has(t)||e.set(t,trustedTypes.createPolicy(t,o)),e.get(t)):null}(t);return r?r.createHTML(o):o}const r="editable-object";class i extends HTMLElement{#e=null;#t=!1;#o=()=>!0;#r=()=>!0;#i=()=>!0;#s=[];#n=[];#l=[];static#a=["object","add-property-placeholder","disable-edit"];static#d={object:{},"add-property-placeholder":"Add new property in key:value format","disabled-edit":!1};static get observedAttributes(){return this.#a}constructor(){super(),this.attachShadow({mode:"open",delegatesFocus:!0})}#c(e){if(this.hasAttribute(e)){const t=this.getAttribute(e);return/^\s*(?:true|false)\s*$/i.test(t)?"false"!==t:t}return i.#d[e]}#p(e){if("string"==typeof e||"number"==typeof e||"boolean"==typeof e||null===e)return e;if(void 0===e||"function"==typeof e||"symbol"==typeof e)return null;if("bigint"==typeof e)return`${e}n`;"[object RegExp]"===Object.prototype.toString.call(e)&&(e={__pattern:e.source,flags:e.flags});let t=JSON.stringify(e);return"{"===t[0]&&(t=t.replaceAll('"',"'")),t}#h(e,t=null){const o=e.trim();let r=parseFloat(o);if(r)return r;if(/\d+n$/.test(o))return BigInt(o.slice(0,-1));if("false"===o.toLowerCase())return!1;if("true"===o.toLowerCase())return!0;if("null"===o.toLowerCase())return null;let i,s=!1;try{let e=o;"{"===e[0]&&(s=!0,e=e.replaceAll("'",'"'));const t=JSON.parse(e);i=Object.keys(t).includes("__pattern")?new RegExp(t.__pattern,t.flags):t}catch{if(s&&t)throw t.classList.add("error"),new Error("Bad object input");i=o}return i}#u(e,o){const r=t(e);return`\n <div class="property-wrapper">\n <label for="eo-${r}-value">${r}</label>\n <input name="${r}" readonly="true" id="eo-${r}-value" type="text" value="${t(o)}" />\n </div>\n <div class="toolbar">\n <button class="editable-object-up-property icon" title="Move up">\n <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">\n <path d="m5 9 1.41 1.41L11 5.83V22h2V5.83l4.59 4.59L19 9l-7-7-7 7z"></path>\n </svg>\n </button>\n <button class="editable-object-down-property icon" title="Move down">\n <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">\n <path d="m19 15-1.41-1.41L13 18.17V2h-2v16.17l-4.59-4.59L5 15l7 7 7-7z"></path>\n </svg>\n </button>\n <button class="editable-object-remove-property icon" title="Remove">\n <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">\n <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path>\n </svg>\n </button>\n </div>\n `}#b(){const e=this.shadowRoot.querySelectorAll(".editable-object-up-property"),t=this.shadowRoot.querySelectorAll(".editable-object-down-property"),o=e.length;for(let r=0;r<o;r++)e[r].style.visibility=0==r?"hidden":"visible",t[r].style.visibility=r==o-1?"hidden":"visible";const r=this.#t?"add":"remove";this.shadowRoot.querySelectorAll(".editable-object-remove-property").forEach(e=>e.classList[r]("hide"))}#y(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}#v(e){for(;"LI"!==e.tagName;)e=e.parentNode;return e}#f(e){this.#w();const t=this.#v(e.target);t.classList.toggle("selected",!0),[...t.querySelectorAll("button")].forEach(e=>{e.tabIndex=0}),t.querySelector("input").focus()}#g(e){return new CustomEvent("change",{bubbles:!0,cancelable:!1,composed:!0,detail:e})}#m(e){"INPUT"!==e.target.nodeName||e.target.classList.contains("error")||"Enter"!==e.key&&" "!==e.key||e.target.click()}#x(e){if(this._editing)return;this._editing=!0;const t=this.#v(e.target).querySelector(".property-wrapper > input");t.classList.remove("error"),t.readOnly=!1,t._value=t.value;const o=this.#j.bind(this),r=this.#j.bind(this);t.addEventListener("blur",o,!1),this.#l.push({host:t,type:"blur",listener:o}),t.addEventListener("keypress",r,!1),this.#l.push({host:t,type:"keypress",listener:r}),t.focus()}#j(e){if(this._editing&&(e instanceof KeyboardEvent&&"Enter"===e.key||!(e instanceof KeyboardEvent))){e.preventDefault();const t=this.#v(e.target),o=t.querySelector(".property-wrapper > label").innerText,r=t.querySelector(".property-wrapper > input"),i=r._value,s=r.value;let n;this._editing=!1,r.readOnly=!0;let l=!1;try{n=this.#h(s,r)}catch{l=!0}this.#l.forEach(e=>{e.host.removeEventListener(e.type,e.listener)}),this.#l.length=0,l?r.classList.add("error"):this.#o(o,n)?(this.#e[o]=n,this.dispatchEvent(this.#g({action:"edit",key:o,previous:this.#h(i),new:n}))):r.classList.add("error")}}#E(e){const t=e.previousElementSibling;return!!t&&(e.parentNode.insertBefore(e,t),this.#b(),e.querySelector(".editable-object-up-property").focus(),!0)}#L(e){const t=e.nextElementSibling;return!!t&&(e.parentNode.insertBefore(t,e),this.#b(),e.querySelector(".editable-object-down-property").focus(),!0)}#k(e){e.remove(),this.#b()}#S(e){e.stopPropagation();const t=this.#v(e.target),o=t.querySelector(".property-wrapper > label").innerText.trim(),r=t.querySelector(".property-wrapper > input"),i=r.value.trim(),s=this.#h(i);this.#r(o,s)?(this.#k(t),delete this.#e[o],this.dispatchEvent(this.#g({action:"remove",key:o,previous:s,new:null}))):r.classList.add("error")}#q(e){e.stopPropagation();const t=this.#v(e.target);this.#E(t)}#A(e){e.stopPropagation();const t=this.#v(e.target);this.#L(t)}#R(){let e,t=0;return function(o){const r=(new Date).getTime(),i=r-t;i<500&&i>0?(o.preventDefault(),this.#x(o)):e=setTimeout(()=>{clearTimeout(e)},500),t=r}.bind(this)}#P(e){const t=this.#f.bind(this);e.forEach(e=>{e.addEventListener("click",t,!1),this.#n.push({host:e,type:"click",listener:t})})}#_(){const e=this.shadowRoot.querySelectorAll(".object-properties .property-wrapper"),t={remove:this.shadowRoot.querySelectorAll('.object-properties button[title="Remove"]')};this.#M(e,t)}#M(e,t){const o=this.#y(),r=this.#R(),i=this.#x.bind(this),s=this.#S.bind(this);if(this.#t){const e=["dblclick","touchend"],t=[];this.#n.filter((o,r)=>{let i=e.includes(o.type);return i?t.push(r):"remove"===o.host.title.toLowerCase()&&(i=!0,t.push(r)),i}).forEach(e=>{e.host.removeEventListener(e.type,e.listener)});let o=0;for(const e of t)this.#n.splice(e-o++,1)}else this._editing=!1,e.forEach(e=>{e.addEventListener("dblclick",i,!1),this.#n.push({host:e,type:"dblclick",listener:i}),o&&(e.addEventListener("touchend",r),this.#n.push({host:e,type:"touchend",listener:r}))}),t.remove.forEach(e=>{e.addEventListener("click",s,!1),this.#n.push({host:e,type:"click",listener:s})})}#O(e,t){const o=this.#m.bind(this),r=this.#q.bind(this),i=this.#A.bind(this);e.forEach(e=>{e.addEventListener("keypress",o,!1),this.#n.push({host:e,type:"keypress",listener:o})}),t.up.forEach(e=>{e.addEventListener("click",r,!1),this.#n.push({host:e,type:"click",listener:r})}),t.down.forEach(e=>{e.addEventListener("click",i,!1),this.#n.push({host:e,type:"click",listener:i})}),this.#M(e,t)}#T(e){e.composedPath().includes(this)||this.shadowRoot.querySelector(".editable-object").classList.add("defocused")}#C(){this.shadowRoot.querySelector(".editable-object").classList.remove("defocused")}#H(e){return e in this.#e}#w(){this.shadowRoot.querySelector(".add-new-object-property-input").classList.toggle("error",!1),[...this.shadowRoot.querySelectorAll("li")].forEach(e=>{const t=e.querySelector(".property-wrapper > input");t.classList.contains("error")&&t._value&&(t.value=t._value),t.classList.toggle("error",!1),e.classList.toggle("selected",!1),[...e.querySelectorAll("button")].forEach(e=>{e.tabIndex=-1})})}#N(e){if(e instanceof KeyboardEvent&&"Enter"===e.key||!(e instanceof KeyboardEvent)){const e=this.shadowRoot.querySelector(".add-new-object-property-input"),t=e.value.trim();if(""!==t){const o=/^\s*(?<property>[^\s:]+)\s*:\s*(?<value>[^$]+)$/,r=t.match(o)?.groups,[i,s]=r?Object.values(r):["",""],n=i.trim(),l=s.trim(),a=()=>{e.classList.add("error"),e.focus()};if(!n||!l||this.#H(n))return void a();let d,c=!1;try{d=this.#h(l,e)}catch{c=!0}if(c)a();else if(this.#i(n,d)){const t={[n]:l};this.mergeObject(t),this.dispatchEvent(this.#g({action:"add",key:n,previous:null,new:d})),this.shadowRoot.querySelector(".object-properties").lastChild.click(),e.value=""}else a()}}}#B(){const{shadowRoot:e}=this,t=e.querySelector(".new-object-property"),o=e.querySelector(".add-new-object-property-input"),r=e.querySelector(".editable-object-add-property");if(this.#t){const e=[t,o,r],i=[];this.#s.filter((t,o)=>{const r=e.includes(t.host);return r&&i.push(o),r}).forEach(e=>{e.host.removeEventListener(e.type,e.listener)});let s=0;for(const e of i)this.#s.splice(e-s++,1);t.classList.add("hide")}else{const e=this.getAttribute("add-property-placeholder");this.addPropertyPlaceholder=e;const i=this.#w.bind(this);t.addEventListener("click",i,!0),this.#s.push({host:t,type:"click",listener:i});const s=this.#N.bind(this);o.addEventListener("keypress",s,!1),this.#s.push({host:o,type:"keypress",listener:s}),r.addEventListener("click",s,!1),this.#s.push({host:r,type:"click",listener:s}),t.classList.remove("hide")}}get object(){return this.#e}set object(e){if(!e)return;this.#e&&Object.keys(this.#e).length>0&&[this.#n,this.#l].forEach(e=>{e.forEach(e=>{e.host.removeEventListener(e.type,e.listener)})});const t=this.shadowRoot.querySelector("#loading"),i=this.shadowRoot.querySelector(".object-properties");i.innerHTML=o(r,"");const s=[],n=[],l={up:[],down:[],remove:[]};for(const[t,a]of Object.entries(e)){const e=document.createElement("li");e.innerHTML=o(r,this.#u(t,this.#p(a))),i.appendChild(e),s.push(e),n.push(e.querySelector(".property-wrapper")),l.up.push(e.querySelector(".editable-object-up-property")),l.down.push(e.querySelector(".editable-object-down-property")),l.remove.push(e.querySelector(".editable-object-remove-property"))}this.#P(s),this.#O(n,l),this.#b(),t.classList.add("hide"),this.#e=e}get addPropertyPlaceholder(){return this.#c("add-property-placeholder")}set addPropertyPlaceholder(e){const t="add-property-placeholder",o=this.shadowRoot.querySelector(".add-new-object-property-input");e?(this.setAttribute(t,e),o.placeholder=e):(this.removeAttribute(t),o.placeholder=i.#d[t])}set disableEdit(e){const t="disable-edit";e?(this.setAttribute(t,!0),this.#t=!0):(this.setAttribute(t,!1),this.#t=!1),this.#B(),this.#_(),this.#b()}get disableEdit(){return this.#c("disable-edit")}set onEdit(e){this.#o="function"==typeof e?e:()=>!0}get onEdit(){return this.#o}set onAdd(e){this.#i="function"==typeof e?e:()=>!0}get onAdd(){return this.#i}set onRemove(e){this.#r="function"==typeof e?e:()=>!0}get onRemove(){return this.#r}mergeObject(e){this.object={...this.#e,...e}}connectedCallback(){const{shadowRoot:e}=this;e.innerHTML=o(r,'<style>:host{--eo-min-width:300px;--eo-bg-color:#fafafa;--eo-border-radius:4px;--eo-border-focused-color:#444;--eo-border-defocused-color:#aaa;--eo-item-selected-bg-color:#999;--eo-item-selected-color:#222;--eo-item-selected-border-radius:4px;--eo-item-hover-border-width:1px;--eo-item-hover-border-color:#ddd;--eo-item-hover-border-radius:4px;--eo-icon-color:#444;--eo-add-new-icon-color:#444;--eo-input-focus-outline-color:#26b;--eo-input-focus-outline-width:1px;--eo-input-focus-outline-style:auto;--eo-input-border-color:#bbb;--eo-input-border-radius:4px;--eo-input-bg-color:#444;--eo-input-color:#eee;--eo-input-font-family:sans-serif;--eo-input-placeholder-color:#aaa}:host(.disabled){pointer-events:none}.editable-object{background:var(--eo-bg-color);border-radius:var(--eo-border-radius);min-width:var(--eo-min-width);display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}@media (min-width:360px){.editable-object{padding:0 .5rem}}@media (min-width:464px){.editable-object{border:1px solid var(--eo-border-focused-color);padding:1rem}.editable-object.defocused{border:1px solid var(--eo-border-defocused-color)}}ul{padding:0;margin:0;width:100%}li{line-height:2;list-style:none;cursor:default;padding:.5rem}input{padding:6px 8px;border-radius:var(--eo-input-border-radius);line-height:1.5;border:1px solid var(--eo-input-border-color);color:var(--eo-input-color);background:var(--eo-input-bg-color);font-family:var(--eo-input-font-family)}input:focus-visible{outline:var(--eo-input-focus-outline-color) var(--eo-input-focus-outline-style) var(--eo-input-focus-outline-width)}input::placeholder{color:var(--eo-input-placeholder-color)}.editable-object:not(.defocused) li.selected{background:var(--eo-item-selected-bg-color);color:var(--eo-item-selected-color);border-radius:var(--eo-item-selected-border-radius)}div>div,li{display:flex;align-items:center;justify-content:space-around}.editable-object:not(.defocused,.touch) li:hover{border:var(--eo-item-hover-border-width) solid var(--eo-item-hover-border-color);border-radius:var(--eo-item-hover-border-radius)}.property-wrapper{display:flex;flex-flow:row wrap;flex-grow:1;align-items:baseline;min-width:10em;touch-action:manipulation}.property-wrapper label{flex:1 1 100%;min-width:8em;max-width:100%}.property-wrapper input{flex:1;min-width:10em}.editable-object.defocused input{opacity:.7}@media (min-width:41.69em){.property-wrapper label{flex-basis:auto;max-width:45%;min-width:15em;padding-right:.5rem}.property-wrapper input{flex:1 1;min-width:16em}}.toolbar{display:flex;gap:1.5rem;padding-left:1.5rem}.editable-object.defocused .toolbar,li:not(.selected) .toolbar{opacity:0;pointer-events:none}.toolbar button{position:relative;fill:var(--eo-icon-color)}.editable-object-add-property.icon{fill:var(--eo-add-new-icon-color)}.toolbar button:hover::before{content:"";position:absolute;width:24px;height:24px;background:rgb(0 0 0 / 10%);left:-4px;top:-4px;border-radius:50%}.icon{background-color:transparent;border:none;cursor:pointer;font-size:0;fill:var(--eo-icon-color);padding:0}.icon svg{width:1rem;height:1rem}.new-object-property{display:flex;flex-flow:row wrap;line-height:2;margin-top:2rem;width:100%}label[for=new-property]{flex:1 0 100%}.add-new-object-property-input{min-width:18em;flex:1}.add-new-object-property-input.error,.property-wrapper input.error{outline:red auto 1px}.hide{display:none}</style><div class="editable-object defocused"><slot id=loading name=loading></slot><ul class=object-properties></ul><div class=new-object-property><label for=new-property>New Property and Value</label> <input id=new-property name=new-property class=add-new-object-property-input type=text placeholder="Add new property in key:value format"><div class=toolbar><button class="editable-object-add-property icon" title=Add><svg version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink width=24 height=24 viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg></button></div></div></div>');const t=this.getAttribute("object");this.object=JSON.parse(t);const i=this.getAttribute("disable-edit");this.#t="true"===i?.toLowerCase();const s=e.querySelector(".editable-object"),n=e.querySelector("#loading"),l=this.#y(),a=this.#T.bind(this),d=this.#C.bind(this);this.object&&n.classList.add("hide"),l&&s.classList.add("touch"),document.addEventListener("click",a,!1),this.#s.push({host:document,type:"click",listener:a}),s.addEventListener("click",d,!0),this.#s.push({host:s,type:"click",listener:d}),this.#B()}disconnectedCallback(){[this.#s,this.#n,this.#l].forEach(e=>{e.forEach(e=>{e.host.removeEventListener(e.type,e.listener)})})}attributeChangedCallback(e,t,o){o!==t&&(this[e]=this.getAttribute(e))}}customElements.define("editable-object",i);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localnerve/editable-object",
|
|
3
|
-
"version": "0.5.0-rc.
|
|
3
|
+
"version": "0.5.0-rc.3",
|
|
4
4
|
"description": "A vanillajs editable-object web component for visual object display and editing",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"browser": "dist/editable-object.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@axe-core/cli": "^4.13.0",
|
|
29
29
|
"@eslint/js": "^10.0.1",
|
|
30
|
-
"@localnerve/web-component-build": "^3.4.0-rc.
|
|
30
|
+
"@localnerve/web-component-build": "^3.4.0-rc.5",
|
|
31
31
|
"@playwright/test": "^1.62.1",
|
|
32
32
|
"babel-loader": "10.1.1",
|
|
33
33
|
"eslint": "^10.9.1",
|