@localnerve/editable-object 0.1.0

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/LICENSE.md ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2025 Alex Grant <alex@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+
9
+ * Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+
13
+ * Neither the name of the LocalNerve, LLC nor the
14
+ names of its contributors may be used to endorse or promote products
15
+ derived from this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL LocalNerve, LLC BE LIABLE FOR ANY
21
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # editable-object
2
+ [![npm version](https://badge.fury.io/js/%40localnerve%2Feditable-object.svg)](http://badge.fury.io/js/%40localnerve%2Feditable-object)
3
+
4
+ > A small, fast, no-dependency, editable object webcomponent.
5
+
6
+ ## Summary
7
+
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
+ Non-browser module exports build helpers (for building CSP rules, etc).
10
+
11
+ Can be a convenient 'todo' app to test data update and mutation on the front end with the least additional ceremony.
12
+
13
+ ## Web Details
14
+ + ~11k full, ~3k gzip
15
+
16
+ ## Events
17
+
18
+ This web component issues a 'changed' CustomEvent when an object property is added, edited, or removed. The format of the `event.detail` is as follows:
19
+
20
+ ```
21
+ {
22
+ action: 'add' | 'edit' | 'remove',
23
+ key: '<the property key name>',
24
+ previous: '<the previous value, null if action: 'add'>',
25
+ new: '<the new value, null if action: 'remove'>'
26
+ }
27
+ ```
28
+
29
+ ## Attributes (and Properties)
30
+
31
+ * `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
+
33
+ > Property name is also `object`.
34
+
35
+ * `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
+
37
+ > Property name is `addPropertyPlaceholder` (camel case).
38
+
39
+ ## Javascript Public Properties and Methods
40
+
41
+ * `object` **Property** - 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).
42
+
43
+ * `addPropertyPlaceholder` **Property** - 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'.
44
+
45
+ * `mergeObject(newObject)` **method** - Call to merge more properties into the underlying object under edit.
46
+
47
+ ## Overridable CSS Variables
48
+
49
+ * `--eo-min-width` - The min-width for the component. Defaults to 300px.
50
+ * `--eo-bg-color` - The overall control background color. Defaults to #fafafa.
51
+ * `--eo-border-radius` - The border-radius of the control. Defaults to 4px.
52
+ * `--eo-border-focused-color` - The color of the control border when focused. Defaults to #444.
53
+ * `--eo-border-defocused-color` - The color of the control border when not focused. Defaults to #aaa.
54
+ * `--eo-item-selected-color` - The background color of the property list item when selected. Defaults to #eee.
55
+ * `--eo-item-hover-color` - The background color of the propert list item when hovered. Defaults to #ddd.
56
+ * `--eo-item-input-border-color` - The border color of 'add' and 'edit' input boxes. Defaults to #bbb.
57
+ * `--eo-icon-color` - The color of the toolbar button icons. Defaults to #444.
58
+
59
+ ## Usage Example
60
+
61
+ ```html
62
+ <editable-object object="{'property1':'value1','property2':'value2'}" add-property-placeholder="Add property in key:value format"></editable-object>
63
+ ```
64
+ See [The test reference](https://github.com/localnerve/editable-object/blob/master/test/fixtures/index.html) for another usage example, run/play with the test reference with `npm run test:server`.
65
+
66
+ ## Non-browser Exports
67
+
68
+ The non-browser version of the module exports methods to help with builds.
69
+
70
+ ### {Promise} getEditableObjectCssText()
71
+
72
+ Asynchronously gets the raw shadow css text.
73
+ Useful for computing the hash for a CSP style rule.
74
+ Returns a Promise that resolves to the full utf8 string of css text.
75
+
76
+ ## License
77
+
78
+ LocalNerve [BSD-3-Clause](https://github.com/localnerve/editable-object/blob/master/LICENSE.md) Licensed
79
+
80
+ ## Contact
81
+
82
+ twitter: @localnerve
83
+ email: alex@localnerve.com
@@ -0,0 +1 @@
1
+ :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-color:#eee;--eo-item-hover-color:#ddd;--eo-item-input-border-color:#bbb;--eo-icon-color:#444}:host(.disabled){pointer-events:none}.editable-object{background:var(--eo-bg-color);border:1px solid var(--eo-border-focused-color);border-radius:var(--eo-border-radius);padding:1rem;min-width:var(--eo-min-width);display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.editable-object.defocused{border:1px solid var(--eo-border-defocused-color)}ul{padding:0;margin:0}li{line-height:2.5;list-style:none;cursor:default}div>div,li{display:flex;align-items:center;justify-content:space-between}.editable-object:not(.defocused) li:hover{background:var(--eo-item-hover-color)}.property-wrapper{display:flex;align-items:baseline}.property-wrapper label{padding-right:1rem}.toolbar{display:flex;gap:1rem;padding-left:1rem}.editable-object.defocused .toolbar,li:not(.selected) .toolbar{opacity:0;pointer-events:none}.toolbar button{position:relative}.toolbar button:hover:before{content:"";position:absolute;width:24px;height:24px;background:rgba(0,0,0,.1);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:16px;height:16px}.new-object-property{margin-top:16px;padding:0 8px 0 2px}.add-new-object-property-input,.edit-object-property-input{padding:6px 8px;border-radius:4px;border:1px solid var(--eo-item-input-border-color);min-width:18em}.editable-object.defocused .add-new-object-property-input{border:1px solid transparent;opacity:.5}.edit-object-property-input{margin-left:-8px}
@@ -0,0 +1,2 @@
1
+ /*! editable-list@0.1.0, 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=[];#o=[];#i=[];static#r=["object","add-property-placeholder"];static#s={object:{},addPropertyPlaceholder:"Add new property in key:value format"};static get observedAttributes(){return this.#r}constructor(){super(),this.attachShadow({mode:"open",delegatesFocus:!0})}#n(t){return this.hasAttribute(t)?this.getAttribute(t):e.#s[t]}#l(e,t){return`\n <div class="property-wrapper">\n <label for="eo-${e}-value">${e}</label>\n <input 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 `}#d(){const e=this.shadowRoot.querySelectorAll(".editable-object-up-property"),t=this.shadowRoot.querySelectorAll(".editable-object-down-property"),o=e.length;for(let i=0;i<o;i++)e[i].style.visibility=0==i?"hidden":"visible",t[i].style.visibility=i==o-1?"hidden":"visible"}#c(e){for(;"LI"!==e.tagName;)e=e.parentNode;return e}#a(e){this.#p();const t=this.#c(e.target);t.classList.toggle("selected",!0),[...t.querySelectorAll("button")].forEach((e=>{e.tabIndex=0}))}#h(e){return new CustomEvent("change",{bubbles:!0,cancelable:!1,composed:!0,detail:e})}#u(e){"INPUT"===e.target.nodeName&&("Enter"!==e.key&&" "!==e.key||e.target.click())}#b(e){if(this._editing)return;this._editing=!0;const t=this.#c(e.target).querySelector(".property-wrapper > input");t.readOnly=!1,t._value=t.value;const o=this.#v.bind(this),i=this.#v.bind(this);t.addEventListener("blur",o,!1),this.#i.push({host:t,type:"blur",listener:o}),t.addEventListener("keypress",i,!1),this.#i.push({host:t,type:"keypress",listener:i}),t.focus()}#v(e){if(this._editing&&(e instanceof KeyboardEvent&&"Enter"===e.key||!(e instanceof KeyboardEvent))){e.preventDefault();const t=this.#c(e.target),o=t.querySelector(".property-wrapper > label").innerText,i=t.querySelector(".property-wrapper > input"),r=i._value,s=i.value;this._editing=!1,i.readOnly=!0,this.#i.forEach((e=>{e.host.removeEventListener(e.type,e.listener)})),this.#i.length=0,this.dispatchEvent(this.#h({action:"edit",key:o,previous:r,new:s}))}}#y(e){const t=e.previousElementSibling;return!!t&&(e.parentNode.insertBefore(e,t),this.#d(),!0)}#w(e){const t=e.nextElementSibling;return!!t&&(e.parentNode.insertBefore(t,e),this.#d(),!0)}#g(e){e.remove(),this.#d()}#m(e){e.stopPropagation();const t=this.#c(e.target),o=t.querySelector(".property-wrapper > label").innerText.trim(),i=t.querySelector(".property-wrapper > input").value.trim();this.#g(t),this.dispatchEvent(this.#h({action:"remove",key:o,previous:i,new:null}))}#f(e){e.stopPropagation();const t=this.#c(e.target);this.#y(t)}#k(e){e.stopPropagation();const t=this.#c(e.target);this.#w(t)}#x(){let e,t=0;return function(o){const i=(new Date).getTime(),r=i-t;r<500&&r>0?(o.preventDefault(),this.#b(o)):e=setTimeout((()=>{clearTimeout(e)}),500),t=i}.bind(this)}#E(e){const t=this.#a.bind(this);e.forEach((e=>{e.addEventListener("click",t,!1),this.#o.push({host:e,type:"click",listener:t})}))}#j(e,t){const o=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),i=this.#x(),r=this.#b.bind(this),s=this.#u.bind(this);e.forEach((e=>{e.addEventListener("keypress",s,!1),this.#o.push({host:e,type:"keypress",listener:s}),e.addEventListener("dblclick",r,!1),this.#o.push({host:e,type:"dblclick",listener:r}),o&&(e.addEventListener("touchend",i),this.#o.push({host:e,type:"touchend",listener:i}))}));const n=this.#f.bind(this),l=this.#k.bind(this),d=this.#m.bind(this);t.up.forEach((e=>{e.addEventListener("click",n,!1),this.#o.push({host:e,type:"click",listener:n})})),t.down.forEach((e=>{e.addEventListener("click",l,!1),this.#o.push({host:e,type:"click",listener:l})})),t.remove.forEach((e=>{e.addEventListener("click",d,!1),this.#o.push({host:e,type:"click",listener:d})}))}#L(e){e.composedPath().includes(this)||this.shadowRoot.querySelector(".editable-object").classList.add("defocused")}#S(){this.shadowRoot.querySelector(".editable-object").classList.remove("defocused")}#q(e){return e in this.#e}#p(){[...this.shadowRoot.querySelectorAll("li")].forEach((e=>{e.classList.toggle("selected",!1),[...e.querySelectorAll("button")].forEach((e=>{e.tabIndex=-1}))}))}#A(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,i]=t.split(":"),r=o.trim(),s=i.trim();if(this.#q(r))return void e.focus();const n={[r]:s};this.mergeObject(n),this.dispatchEvent(this.#h({action:"add",key:r,previous:null,new:s})),this.shadowRoot.querySelector(".object-properties").lastChild.click(),e.value=""}}}get object(){return this.#e}set object(e){if(!e)return;this.#e&&Object.keys(this.#e).length>0&&[this.#o,this.#i].forEach((e=>{e.forEach((e=>{e.host.removeEventListener(e.type,e.listener)}))}));const t=this.shadowRoot.querySelector(".object-properties");t.innerHTML="";const o=[],i=[],r={up:[],down:[],remove:[]};for(const[s,n]of Object.entries(e)){const e=document.createElement("li");e.innerHTML=this.#l(s,n),t.appendChild(e),o.push(e),i.push(e.querySelector(".property-wrapper")),r.up.push(e.querySelector(".editable-object-up-property")),r.down.push(e.querySelector(".editable-object-down-property")),r.remove.push(e.querySelector(".editable-object-remove-property"))}this.#E(o),this.#j(i,r),this.#d();const s=o[0].querySelector("input");s.click(),s.focus(),this.#e=e}get addPropertyPlaceholder(){return this.#n("add-property-placeholder")}set addPropertyPlaceholder(e){const t="add-property-placeholder";e?this.setAttribute(t,e):this.removeAttribute(t)}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-color:#eee;--eo-item-hover-color:#ddd;--eo-item-input-border-color:#bbb;--eo-icon-color:#444}:host(.disabled){pointer-events:none}.editable-object{background:var(--eo-bg-color);border:1px solid var(--eo-border-focused-color);border-radius:var(--eo-border-radius);padding:1rem;min-width:var(--eo-min-width);display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.editable-object.defocused{border:1px solid var(--eo-border-defocused-color)}ul{padding:0;margin:0}li{line-height:2.5;list-style:none;cursor:default}div>div,li{display:flex;align-items:center;justify-content:space-between}.editable-object:not(.defocused) li:hover{background:var(--eo-item-hover-color)}.property-wrapper{display:flex;align-items:baseline}.property-wrapper label{padding-right:1rem}.toolbar{display:flex;gap:1rem;padding-left:1rem}.editable-object.defocused .toolbar,li:not(.selected) .toolbar{opacity:0;pointer-events:none}.toolbar button{position:relative}.toolbar button:hover:before{content:"";position:absolute;width:24px;height:24px;background:rgba(0,0,0,.1);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:16px;height:16px}.new-object-property{margin-top:16px;padding:0 8px 0 2px}.add-new-object-property-input,.edit-object-property-input{padding:6px 8px;border-radius:4px;border:1px solid var(--eo-item-input-border-color);min-width:18em}.editable-object.defocused .add-new-object-property-input{border:1px solid transparent;opacity:.5}.edit-object-property-input{margin-left:-8px}</style><div class="editable-object defocused"><ul class=object-properties></ul><div class=new-object-property><input 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=e.querySelector(".editable-object"),i=e.querySelector(".new-object-property"),r=e.querySelector(".add-new-object-property-input"),s=e.querySelector(".editable-object-add-property"),n=this.#L.bind(this),l=this.#S.bind(this);document.addEventListener("click",n,!1),this.#t.push({host:document,type:"click",listener:n}),o.addEventListener("click",l,!0),this.#t.push({host:o,type:"click",listener:l});const d=this.#p.bind(this),c=this.#A.bind(this);i.addEventListener("click",d,!0),this.#t.push({host:i,type:"click",listener:d}),r.addEventListener("keypress",c,!1),this.#t.push({host:r,type:"keypress",listener:c}),s.addEventListener("click",c,!1),this.#t.push({host:s,type:"click",listener:c})}disconnectedCallback(){[this.#t,this.#o,this.#i].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/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * editable-object Node entry point
3
+ *
4
+ * Copyright (c) 2025 Alex Grant (@localnerve), LocalNerve LLC
5
+ * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
6
+ */
7
+ import * as path from 'node:path';
8
+ import * as fs from 'node:fs/promises';
9
+ import * as url from 'node:url';
10
+
11
+ const thisDir = url.fileURLToPath(new URL('.', import.meta.url));
12
+
13
+ /**
14
+ * Get the css file contents.
15
+ * Useful for CSP builds.
16
+ *
17
+ * @returns {Promise<String>} The utf8 css file content
18
+ */
19
+ export function getEditableObjectCssText () {
20
+ return fs.readFile(path.join(thisDir, 'editable-object.css'), {
21
+ encoding: 'utf8'
22
+ });
23
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@localnerve/editable-object",
3
+ "version": "0.1.0",
4
+ "description": "A vanillajs editable-object web component for visual object display and editing",
5
+ "main": "dist/index.js",
6
+ "browser": "dist/editable-object.js",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build:debug": "pushd src && node --inspect-brk build.js && popd",
10
+ "build": "pushd src && node build.js && webpack-cli --config webpack.prod.config.js && popd",
11
+ "lint": "eslint .",
12
+ "prepublishOnly": "npm run build",
13
+ "test:server": "SKIP_MIN=1 npm run build && node test/dev-server.js"
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/js": "^9.28.0",
17
+ "@localnerve/web-component-build": "^1.11.0",
18
+ "babel-loader": "10.0.0",
19
+ "eslint": "^9.28.0",
20
+ "express": "5.1.0",
21
+ "globals": "^16.2.0",
22
+ "webpack-cli": "6.0.1"
23
+ },
24
+ "files": [
25
+ "dist/**"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/localnerve/editable-object.git"
30
+ },
31
+ "keywords": [
32
+ "editable",
33
+ "list",
34
+ "webcomponent",
35
+ "javascript",
36
+ "vanillajs"
37
+ ],
38
+ "author": "Alex Grant <alex@localnerve.com> (https://www.localnerve.com)",
39
+ "license": "BSD-3-Clause",
40
+ "bugs": {
41
+ "url": "https://github.com/localnerve/editable-object/issues"
42
+ },
43
+ "homepage": "https://github.com/localnerve/editable-object/tree/master/src/editable-object#readme"
44
+ }