@localnerve/editable-object 0.3.10 → 0.3.12
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 -7
- package/dist/editable-object.js +2 -2
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
# editable-object
|
|
2
2
|
[](http://badge.fury.io/js/%40localnerve%2Feditable-object)
|
|
3
3
|
|
|
4
|
-
> A small, fast, no-dependency, editable object
|
|
4
|
+
> A small, fast, no-dependency, editable object web component.
|
|
5
5
|
|
|
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
9
|
Non-browser module exports build helpers (for building CSP rules, etc).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
_A convenient, **no-dependency** drop-in 'todo' app component to test/round-trip data updates and mutations on the front end._
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
13
|
+
## Quick Links
|
|
14
|
+
|
|
15
|
+
* [Events](#events)
|
|
16
|
+
* [Attributes](#attributes-and-properties)
|
|
17
|
+
* [Named Slots](#named-slots)
|
|
18
|
+
* [Properties and Methods](#javascript-public-properties-and-methods)
|
|
19
|
+
* [CSS Variables](#overridable-css-variables)
|
|
20
|
+
* [Usage Examples](#usage-example)
|
|
21
|
+
* [Non-browser exports](#nonbrowser-exports)
|
|
15
22
|
|
|
16
23
|
## Events
|
|
17
24
|
|
|
18
|
-
This web component issues a 'changed' CustomEvent when an object property is added, edited, or removed.
|
|
25
|
+
This web component issues a 'changed' CustomEvent when an object property is added, edited, or removed.
|
|
26
|
+
The format of the `event.detail` is as follows:
|
|
19
27
|
|
|
20
28
|
```
|
|
21
29
|
{
|
|
@@ -26,37 +34,49 @@ This web component issues a 'changed' CustomEvent when an object property is add
|
|
|
26
34
|
}
|
|
27
35
|
```
|
|
28
36
|
|
|
37
|
+
_[Event Usage Example](test/fixtures/handlers.html)_
|
|
38
|
+
|
|
29
39
|
## Attributes (and Properties)
|
|
30
40
|
|
|
31
41
|
* `object` - *Optional*. The initial object to edit - Must be a JSON stringified object. Can be added later without JSON stringification via the javascript property `object`.
|
|
32
42
|
|
|
33
|
-
> Property name is also `object`.
|
|
43
|
+
> Property name is also `object`.
|
|
44
|
+
- [object Usage Example](test/fixtures/repeat-assign.html)
|
|
34
45
|
|
|
35
46
|
* `add-property-placeholder` - *Optional*. The text that prompts a user to add a new property to the object. Defaults to 'Add new property in key:value format'.
|
|
36
47
|
|
|
37
48
|
> Property name is `addPropertyPlaceholder`.
|
|
49
|
+
- [addPropertyPlaceholder Usage Example](test/fixtures/add-property-placeholder.html)
|
|
38
50
|
|
|
39
51
|
* `disable-edit` - *Optional*. Disallow the editing functions. Makes this component a read-only view of the object.
|
|
40
52
|
|
|
41
53
|
> Property name is `disableEdit`.
|
|
54
|
+
- [disableEdit Usage Example](test/fixtures/disable-edit.html)
|
|
42
55
|
|
|
43
56
|
## Named Slots
|
|
44
57
|
|
|
45
58
|
* `"loading"` - *Optional*. A named slot you can use to bring in content to display during loading. Hidden after initial object parse or later when object is set.
|
|
59
|
+
- [Slot Usage Example](test/fixtures/spinner.html)
|
|
46
60
|
|
|
47
61
|
## Javascript Public Properties and Methods
|
|
48
62
|
|
|
49
63
|
* **Property** `object` {**Object**} - Assign a javascript `Object` to set the component's internals for editing. Any existing object is replaced. JSON compatible properties only (string, number, boolean, array, object, null).
|
|
64
|
+
- [object Usage Example](test/fixtures/repeat-assign.html)
|
|
50
65
|
|
|
51
66
|
* **Property** `addPropertyPlaceholder` {**String**} - Assign a prompt to show the user in the new property/value input box to override the default 'Add new property in key:value format'.
|
|
67
|
+
- [addPropertyPlaceholder Usage Example](test/fixtures/add-property-placeholder.html)
|
|
52
68
|
|
|
53
69
|
* **Property** `disableEdit` {**Boolean**} - Assign to true to make the control read-only and disallow any editing.
|
|
70
|
+
- [disableEdit Usage Example](test/fixtures/disable-edit.html)
|
|
54
71
|
|
|
55
72
|
* **Property** `onEdit` {**Function**} - Assign to a javascript function to be called on edit. Use to supply custom validation to an object property value before edit. Receives the property name and proposed new value from the user. Return true to allow the edit to proceed, or false to invalidate it.
|
|
73
|
+
- [onEdit Usage Example](test/fixtures/handlers.html)
|
|
56
74
|
|
|
57
75
|
* **Property** `onAdd` {**Function**} - Assign to a javascript function to be called on add. Use to supply custom validation to an object property value before add. Receives the new property name and proposed value from the user. Return true to allow the add to proceed, or false to invalidate it.
|
|
76
|
+
- [onAdd Usage Example](test/fixtures/handlers.html)
|
|
58
77
|
|
|
59
78
|
* **Property** `onRemove` {**Function**} - Assign to a javascript function to be called on remove. Can be used to supply custom validation to allow a property to be deleted. Receives the property name and value. Return true to allow the delete to proceed, or false to stop it.
|
|
79
|
+
- [onRemove Usage Example](test/fixtures/handlers.html)
|
|
60
80
|
|
|
61
81
|
* **Method** `mergeObject(newObject)` - Call to merge more properties into the underlying object under edit.
|
|
62
82
|
|
|
@@ -107,6 +127,8 @@ Asynchronously gets the raw shadow css text.
|
|
|
107
127
|
Useful for computing the hash for a CSP style rule.
|
|
108
128
|
Returns a Promise that resolves to the full utf8 string of css text.
|
|
109
129
|
|
|
130
|
+
- [getEditableObjectCssText Usage Example](https://github.com/localnerve/jam-build/blob/main/src/build/html.js#L24)
|
|
131
|
+
|
|
110
132
|
## License
|
|
111
133
|
|
|
112
134
|
LocalNerve [BSD-3-Clause](https://github.com/localnerve/editable-object/blob/master/LICENSE.md) Licensed
|
package/dist/editable-object.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! editable-object@0.3.
|
|
2
|
-
class e extends HTMLElement{#e=null;#t=!1;#o=()=>!0;#r=()=>!0;#i=()=>!0;#s=[];#n=[];#l=[];static#d=["object","add-property-placeholder","disable-edit"];static#a={object:{},"add-property-placeholder":"Add new property in key:value format","disabled-edit":!1};static get observedAttributes(){return this.#d}constructor(){super(),this.attachShadow({mode:"open",delegatesFocus:!0})}#c(t){if(this.hasAttribute(t)){const e=this.getAttribute(t);return/^\s*(?:true|false)\s*$/i.test(e)?"false"!==e:e}return e.#a[t]}#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,t){return`\n <div class="property-wrapper">\n <label for="eo-${e}-value">${e}</label>\n <input name="${e}" readonly="true" id="eo-${e}-value" type="text" value="${t}" />\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)}#k(e){const t=e.nextElementSibling;return!!t&&(e.parentNode.insertBefore(t,e),this.#b(),e.querySelector(".editable-object-down-property").focus(),!0)}#L(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.#L(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.#k(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})})}#O(){const e=this.shadowRoot.querySelectorAll(".object-properties .property-wrapper"),t={remove:this.shadowRoot.querySelectorAll('.object-properties button[title="Remove"]')};this.#_(e,t)}#_(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 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})})}#C(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.#_(e,t)}#M(e){e.composedPath().includes(this)||this.shadowRoot.querySelector(".editable-object").classList.add("defocused")}#T(){this.shadowRoot.querySelector(".editable-object").classList.remove("defocused")}#N(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})})}#B(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(),d=()=>{e.classList.add("error"),e.focus()};if(!n||!l||this.#N(n))return void d();let a,c=!1;try{a=this.#h(l,e)}catch{c=!0}if(c)d();else if(this.#i(n,a)){const t={[n]:l};this.mergeObject(t),this.dispatchEvent(this.#g({action:"add",key:n,previous:null,new:a})),this.shadowRoot.querySelector(".object-properties").lastChild.click(),e.value=""}else d()}}}#$(){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.#B.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"),o=this.shadowRoot.querySelector(".object-properties");o.innerHTML="";const r=[],i=[],s={up:[],down:[],remove:[]};for(const[t,n]of Object.entries(e)){const e=document.createElement("li");e.innerHTML=this.#u(t,this.#p(n)),o.appendChild(e),r.push(e),i.push(e.querySelector(".property-wrapper")),s.up.push(e.querySelector(".editable-object-up-property")),s.down.push(e.querySelector(".editable-object-down-property")),s.remove.push(e.querySelector(".editable-object-remove-property"))}this.#P(r),this.#C(i,s),this.#b(),t.classList.add("hide"),this.#e=e}get addPropertyPlaceholder(){return this.#c("add-property-placeholder")}set addPropertyPlaceholder(t){const o="add-property-placeholder",r=this.shadowRoot.querySelector(".add-new-object-property-input");t?(this.setAttribute(o,t),r.placeholder=t):(this.removeAttribute(o),r.placeholder=e.#a[o])}set disableEdit(e){const t="disable-edit";e?(this.setAttribute(t,!0),this.#t=!0):(this.setAttribute(t,!1),this.#t=!1),this.#$(),this.#O(),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='<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 o=this.getAttribute("disable-edit");this.#t="true"===o?.toLowerCase();const r=e.querySelector(".editable-object"),i=e.querySelector("#loading"),s=this.#y(),n=this.#M.bind(this),l=this.#T.bind(this);this.object&&i.classList.add("hide"),s&&r.classList.add("touch"),document.addEventListener("click",n,!1),this.#s.push({host:document,type:"click",listener:n}),r.addEventListener("click",l,!0),this.#s.push({host:r,type:"click",listener:l}),this.#$()}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",e);
|
|
1
|
+
/*! editable-object@0.3.12, Copyright (c) 2025 Alex Grant <alex@localnerve.com> (https://www.localnerve.com), LocalNerve LLC, BSD-3-Clause */
|
|
2
|
+
class e extends HTMLElement{#e=null;#t=!1;#o=()=>!0;#r=()=>!0;#i=()=>!0;#s=[];#n=[];#l=[];static#d=["object","add-property-placeholder","disable-edit"];static#a={object:{},"add-property-placeholder":"Add new property in key:value format","disabled-edit":!1};static get observedAttributes(){return this.#d}constructor(){super(),this.attachShadow({mode:"open",delegatesFocus:!0})}#c(t){if(this.hasAttribute(t)){const e=this.getAttribute(t);return/^\s*(?:true|false)\s*$/i.test(e)?"false"!==e:e}return e.#a[t]}#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,t){return`\n <div class="property-wrapper">\n <label for="eo-${e}-value">${e}</label>\n <input name="${e}" readonly="true" id="eo-${e}-value" type="text" value="${t}" />\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)}#k(e){const t=e.nextElementSibling;return!!t&&(e.parentNode.insertBefore(t,e),this.#b(),e.querySelector(".editable-object-down-property").focus(),!0)}#L(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.#L(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.#k(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.#O(e,t)}#O(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})})}#C(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.#O(e,t)}#M(e){e.composedPath().includes(this)||this.shadowRoot.querySelector(".editable-object").classList.add("defocused")}#T(){this.shadowRoot.querySelector(".editable-object").classList.remove("defocused")}#N(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})})}#B(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(),d=()=>{e.classList.add("error"),e.focus()};if(!n||!l||this.#N(n))return void d();let a,c=!1;try{a=this.#h(l,e)}catch{c=!0}if(c)d();else if(this.#i(n,a)){const t={[n]:l};this.mergeObject(t),this.dispatchEvent(this.#g({action:"add",key:n,previous:null,new:a})),this.shadowRoot.querySelector(".object-properties").lastChild.click(),e.value=""}else d()}}}#$(){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.#B.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"),o=this.shadowRoot.querySelector(".object-properties");o.innerHTML="";const r=[],i=[],s={up:[],down:[],remove:[]};for(const[t,n]of Object.entries(e)){const e=document.createElement("li");e.innerHTML=this.#u(t,this.#p(n)),o.appendChild(e),r.push(e),i.push(e.querySelector(".property-wrapper")),s.up.push(e.querySelector(".editable-object-up-property")),s.down.push(e.querySelector(".editable-object-down-property")),s.remove.push(e.querySelector(".editable-object-remove-property"))}this.#P(r),this.#C(i,s),this.#b(),t.classList.add("hide"),this.#e=e}get addPropertyPlaceholder(){return this.#c("add-property-placeholder")}set addPropertyPlaceholder(t){const o="add-property-placeholder",r=this.shadowRoot.querySelector(".add-new-object-property-input");t?(this.setAttribute(o,t),r.placeholder=t):(this.removeAttribute(o),r.placeholder=e.#a[o])}set disableEdit(e){const t="disable-edit";e?(this.setAttribute(t,!0),this.#t=!0):(this.setAttribute(t,!1),this.#t=!1),this.#$(),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='<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 o=this.getAttribute("disable-edit");this.#t="true"===o?.toLowerCase();const r=e.querySelector(".editable-object"),i=e.querySelector("#loading"),s=this.#y(),n=this.#M.bind(this),l=this.#T.bind(this);this.object&&i.classList.add("hide"),s&&r.classList.add("touch"),document.addEventListener("click",n,!1),this.#s.push({host:document,type:"click",listener:n}),r.addEventListener("click",l,!0),this.#s.push({host:r,type:"click",listener:l}),this.#$()}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",e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localnerve/editable-object",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"axe": "axe http://localhost:3010",
|
|
10
10
|
"build:debug": "pushd src && node --inspect-brk build.js && popd",
|
|
11
11
|
"build": "cd src && node build.js && webpack-cli --config webpack.prod.config.js && cd -",
|
|
12
|
+
"build:dev": "cd src && node build.js && webpack-cli --config webpack.dev.config.js && cd -",
|
|
12
13
|
"clean": "rimraf src/tmp",
|
|
13
14
|
"lint": "npm run clean && eslint . && stylelint -f verbose src/**/*.css",
|
|
14
15
|
"prepublishOnly": "npm run build",
|
|
@@ -18,10 +19,10 @@
|
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@axe-core/cli": "^4.10.2",
|
|
21
|
-
"@eslint/js": "^9.
|
|
22
|
-
"@localnerve/web-component-build": "^1.
|
|
22
|
+
"@eslint/js": "^9.34.0",
|
|
23
|
+
"@localnerve/web-component-build": "^1.13.0",
|
|
23
24
|
"babel-loader": "10.0.0",
|
|
24
|
-
"eslint": "^9.
|
|
25
|
+
"eslint": "^9.34.0",
|
|
25
26
|
"express": "5.1.0",
|
|
26
27
|
"globals": "^16.3.0",
|
|
27
28
|
"modern-normalize": "^3.0.1",
|