├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── spellright.dict ├── LICENSE ├── README.md ├── docs ├── app.1bb0a54e.js ├── app.1bb0a54e.js.map ├── app.6fe1815b.js ├── app.6fe1815b.js.map ├── app.cb2ef314.css ├── app.cb2ef314.css.map ├── app.e2170d7c.css ├── app.e2170d7c.css.map ├── app.e551f8c0.js ├── app.e551f8c0.js.map ├── app.f6c559f6.js ├── app.f6c559f6.js.map └── index.html ├── lerna.json ├── package.json ├── packages ├── example │ ├── .editorconfig │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── app.ts │ │ ├── components │ │ │ ├── layout.ts │ │ │ └── tree-view.ts │ │ ├── styles.css │ │ └── utils │ │ │ └── index.ts │ ├── tsconfig.json │ └── tslint.json └── mithril-tree-component │ ├── .editorconfig │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── cssnano.config.js │ ├── img │ └── mithril-tree-component-animation.gif │ ├── package-lock.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── rollup.config.js │ ├── src │ ├── declarations │ │ ├── css.d.ts │ │ └── svg.d.ts │ ├── index.ts │ ├── models │ │ ├── index.ts │ │ ├── tree-item.ts │ │ ├── tree-options.ts │ │ └── tree-state.ts │ ├── styles │ │ └── tree-container.css │ ├── tree-container.ts │ ├── tree-item.ts │ └── utils │ │ └── index.ts │ ├── tsconfig.json │ └── tslint.json └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | .rpt2_cache 63 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:62608", 12 | "webRoot": "${workspaceFolder}/packages/example/dist", 13 | "skipFiles": ["jquery.js"], 14 | "smartStep": true, 15 | "sourceMapPathOverrides": { 16 | "../node_modules/mithril-tree-component/dist/*": "${webRoot}/../../mithril-tree-component/dist/*" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": "en", 3 | "spellright.documentTypes": [ 4 | "markdown", 5 | "latex", 6 | "plaintext" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/spellright.dict: -------------------------------------------------------------------------------- 1 | onclick 2 | Callback 3 | Lerna 4 | async 5 | pnpm 6 | unpkg 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Erik Vullings 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mithril-tree-component 2 | 3 | A tree component for [Mithril](https://mithril.js.org) that supports drag-and-drop, as well as selecting, creating and deleting items. You can play with it [here](https://erikvullings.github.io/mithril-tree-component/#!/). 4 | 5 | ![Mithril-tree-component animation](./packages/mithril-tree-component/img/mithril-tree-component-animation.gif) 6 | 7 | **Functionality:** 8 | 9 | - Drag-and-drop to move items (if `editable.canUpdate` is true). 10 | - Create and delete tree items (if `editable.canDelete` and `editable.canDeleteParent` is true). 11 | - Configurable properties for: 12 | - `id` property: unique id of the item. 13 | - `parentId` property: id of the parent. 14 | - `name` property: display title. Alternatively, provide your own component. 15 | - `maxDepth`: when specified, and editable.canCreate is true, do not add children that would exceed this depth, where depth is 0 for root items, 1 for children, etc. 16 | - `isOpen`: to indicate whether the tree should show the children. If not provided, the open/close state is maintained internally. This slightly affects the behaviour of the tree, e.g. after creating items, the parent is not automatically opened. 17 | - `create`: can be used to add your own TreeItem creation logic. 18 | - Callback events: 19 | - `onSelect`: when a tree item is selected. 20 | - `onBefore`[Create | Update | Delete]: can be used to intercept (and block) tree item actions. If the onBeforeX call returns false, the action is stopped. 21 | - `on[Create | Update | Delete]`: when the creation is done. 22 | - When using async functions or promises, please make sure to call `m.redraw()` when you are done. 23 | 24 | This repository contains two projects: 25 | 26 | - An example project, showcasing the usage of the component. 27 | - The mithril-tree-component itself. 28 | 29 | ## Installation 30 | 31 | The tree component is available on [npm](https://www.npmjs.com/package/mithril-tree-component) or can be used directly from [unpkg](https://unpkg.com/mithril-tree-component). 32 | 33 | ```bash 34 | npm i mithril-tree-component 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```bash 40 | npm i mithril-tree-component 41 | ``` 42 | 43 | From the [example project](../example). There you can also find some CSS styles. 44 | 45 | ```ts 46 | import m from 'mithril'; 47 | import { unflatten } from '../utils'; 48 | import { TreeContainer, ITreeOptions, ITreeItem, uuid4 } from 'mithril-tree-component'; 49 | 50 | interface IMyTree extends ITreeItem { 51 | id: number | string; 52 | parentId: number | string; 53 | title: string; 54 | } 55 | 56 | export const TreeView = () => { 57 | const data: IMyTree[] = [ 58 | { id: 1, parentId: 0, title: 'My id is 1' }, 59 | { id: 2, parentId: 1, title: 'My id is 2' }, 60 | { id: 3, parentId: 1, title: 'My id is 3' }, 61 | { id: 4, parentId: 2, title: 'My id is 4' }, 62 | { id: 5, parentId: 0, title: 'My id is 5' }, 63 | { id: 6, parentId: 0, title: 'My id is 6' }, 64 | { id: 7, parentId: 4, title: 'My id is 7' }, 65 | ]; 66 | const tree = unflatten(data); 67 | const options = { 68 | id: 'id', 69 | parentId: 'parentId', 70 | isOpen: 'isOpen', 71 | name: 'title', 72 | onSelect: (ti, isSelected) => console.log(`On ${isSelected ? 'select' : 'unselect'}: ${ti.title}`), 73 | onBeforeCreate: ti => console.log(`On before create ${ti.title}`), 74 | onCreate: ti => console.log(`On create ${ti.title}`), 75 | onBeforeDelete: ti => console.log(`On before delete ${ti.title}`), 76 | onDelete: ti => console.log(`On delete ${ti.title}`), 77 | onBeforeUpdate: (ti, action, newParent) => 78 | console.log(`On before ${action} update ${ti.title} to ${newParent ? newParent.title : ''}.`), 79 | onUpdate: ti => console.log(`On update ${ti.title}`), 80 | create: (parent?: IMyTree) => { 81 | const item = {} as IMyTree; 82 | item.id = uuid4(); 83 | if (parent) { 84 | item.parentId = parent.id; 85 | } 86 | item.title = `Created at ${new Date().toLocaleTimeString()}`; 87 | return item as ITreeItem; 88 | }, 89 | editable: { canCreate: true, canDelete: true, canUpdate: true, canDeleteParent: false }, 90 | } as ITreeOptions; 91 | return { 92 | view: () => 93 | m('.row', [ 94 | m('.col.s6', [m('h3', 'Mithril-tree-component'), m(TreeContainer, { tree, options })]), 95 | m('.col.s6', [m('h3', 'Tree data'), m('pre', m('code', JSON.stringify(tree, null, 2)))]), 96 | ]), 97 | }; 98 | }; 99 | ``` 100 | 101 | ## Development 102 | 103 | I prefer to use [pnpm](https://pnpm.js.org/) to install all libs only once on my PC, so if you aren't using it yet, please try it out. 104 | 105 | ```bash 106 | npm i -g pnpm 107 | pnpm m i 108 | npm start 109 | ``` 110 | 111 | ## Contributing 112 | 113 | Pull requests and stars are always welcome. 114 | 115 | For bugs and feature requests, please create an issue. 116 | 117 | 1. Fork it! 118 | 2. Create your feature branch: `git checkout -b my-new-feature` 119 | 3. Commit your changes: `git commit -am 'Add some feature'` 120 | 4. Push to the branch: `git push origin my-new-feature` 121 | 5. Submit a pull request :D 122 | -------------------------------------------------------------------------------- /docs/app.1bb0a54e.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c0&&(l.className=i.join(" ")),o[e]={tag:n,attrs:l}}function u(e,n){var r=n.attrs,o=t.normalizeChildren(n.children),a=i.call(r,"class"),u=a?r.class:r.className;if(n.tag=e.tag,n.attrs=null,n.children=void 0,!l(e.attrs)&&!l(r)){var s={};for(var f in r)i.call(r,f)&&(s[f]=r[f]);r=s}for(var f in e.attrs)i.call(e.attrs,f)&&"className"!==f&&!i.call(r,f)&&(r[f]=e.attrs[f]);for(var f in null==u&&null==e.attrs.className||(r.className=null!=u?null!=e.attrs.className?String(e.attrs.className)+" "+String(u):u:null!=e.attrs.className?e.attrs.className:null),a&&(r.class=null),r)if(i.call(r,f)&&"key"!==f){n.attrs=r;break}return Array.isArray(o)&&1===o.length&&null!=o[0]&&"#"===o[0].tag?n.text=o[0].children:n.children=o,n}function s(e){if(null==e||"string"!=typeof e&&"function"!=typeof e&&"function"!=typeof e.view)throw Error("The selector must be either a string or a component.");var r=n.apply(1,arguments);return"string"==typeof e&&(r.children=t.normalizeChildren(r.children),"["!==e)?u(o[e]||a(e),r):(r.tag=e,r)}s.trust=function(e){return null==e&&(e=""),t("<",void 0,void 0,e,void 0,void 0)},s.fragment=function(){var e=n.apply(0,arguments);return e.tag="[",e.children=t.normalizeChildren(e.children),e};var f=function(){return s.apply(this,arguments)};if(f.m=s,f.trust=s.trust,f.fragment=s.fragment,(c=function(e){if(!(this instanceof c))throw new Error("Promise must be called with `new`");if("function"!=typeof e)throw new TypeError("executor must be a function");var t=this,n=[],r=[],o=u(n,!0),i=u(r,!1),l=t._instance={resolvers:n,rejectors:r},a="function"==typeof setImmediate?setImmediate:setTimeout;function u(e,o){return function u(f){var c;try{if(!o||null==f||"object"!=typeof f&&"function"!=typeof f||"function"!=typeof(c=f.then))a(function(){o||0!==e.length||console.error("Possible unhandled promise rejection:",f);for(var t=0;t0||e(n)}}var r=n(i);try{e(n(o),r)}catch(l){r(l)}}s(e)}).prototype.then=function(e,t){var n,r,o=this._instance;function i(e,t,i,l){t.push(function(t){if("function"!=typeof e)i(t);else try{n(e(t))}catch(o){r&&r(o)}}),"function"==typeof o.retry&&l===o.state&&o.retry()}var l=new c(function(e,t){n=e,r=t});return i(e,o.resolvers,n,!0),i(t,o.rejectors,r,!1),l},c.prototype.catch=function(e){return this.then(null,e)},c.prototype.finally=function(e){return this.then(function(t){return c.resolve(e()).then(function(){return t})},function(t){return c.resolve(e()).then(function(){return c.reject(t)})})},c.resolve=function(e){return e instanceof c?e:new c(function(t){t(e)})},c.reject=function(e){return new c(function(t,n){n(e)})},c.all=function(e){return new c(function(t,n){var r=e.length,o=0,i=[];if(0===e.length)t([]);else for(var l=0;l=200&&c.status<300||304===c.status||/^file:\/\//i.test(t),i=c.responseText;if("function"==typeof n.extract)i=n.extract(c,n),e=!0;else if("function"==typeof n.deserialize)i=n.deserialize(i);else try{i=i?JSON.parse(i):null}catch(a){throw new Error("Invalid JSON: "+i)}if(e)r(i);else{var l=new Error(c.responseText);l.code=c.status,l.response=i,o(l)}}catch(a){o(a)}},u&&null!=s?c.send(s):c.send()}),jsonp:o(function(t,n,o,i){var a=n.callbackName||"_mithril_"+Math.round(1e16*Math.random())+"_"+r++,u=e.document.createElement("script");e[a]=function(t){u.parentNode.removeChild(u),o(t),delete e[a]},u.onerror=function(){u.parentNode.removeChild(u),i(new Error("JSONP request failed")),delete e[a]},t=l(t,n.data,!0),u.src=t+(t.indexOf("?")<0?"?":"&")+encodeURIComponent(n.callbackKey||"callback")+"="+encodeURIComponent(a),e.document.documentElement.appendChild(u)}),setCompletionCallback:function(e){n=e}}},v=p(window,c),h=function(e){var n,r=e.document,o={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function i(e){return e.attrs&&e.attrs.xmlns||o[e.tag]}function l(e,t){if(e.state!==t)throw new Error("`vnode.state` must not be modified")}function a(e){var t=e.state;try{return this.apply(t,arguments)}finally{l(e,t)}}function u(){try{return r.activeElement}catch(e){return null}}function s(e,t,n,r,o,i,l){for(var a=n;a'+t.children+"",l=l.firstChild):l.innerHTML=t.children,t.dom=l.firstChild,t.domSize=l.childNodes.length;for(var a,u=r.createDocumentFragment();a=l.firstChild;)u.appendChild(a);g(e,u,o)}function p(e,t,n,r,o,i){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)s(e,n,0,n.length,r,o,i);else if(null==n||0===n.length)x(t,0,t.length);else{for(var l=0,a=0,u=null,c=null;a=a&&z>=l;)if(w=t[C],k=n[z],null==w)C--;else if(null==k)z--;else{if(w.key!==k.key)break;w!==k&&v(e,w,k,r,o,i),null!=k.dom&&(o=k.dom),C--,z--}for(;C>=a&&z>=l;)if(d=t[a],p=n[l],null==d)a++;else if(null==p)l++;else{if(d.key!==p.key)break;a++,l++,d!==p&&v(e,d,p,r,y(t,a,o),i)}for(;C>=a&&z>=l;){if(null==d)a++;else if(null==p)l++;else if(null==w)C--;else if(null==k)z--;else{if(l===z)break;if(d.key!==k.key||w.key!==p.key)break;S=y(t,a,o),g(e,m(w),S),w!==p&&v(e,w,p,r,S,i),++l<=--z&&g(e,m(d),o),d!==k&&v(e,d,k,r,o,i),null!=k.dom&&(o=k.dom),a++,C--}w=t[C],k=n[z],d=t[a],p=n[l]}for(;C>=a&&z>=l;){if(null==w)C--;else if(null==k)z--;else{if(w.key!==k.key)break;w!==k&&v(e,w,k,r,o,i),null!=k.dom&&(o=k.dom),C--,z--}w=t[C],k=n[z]}if(l>z)x(t,a,C+1);else if(a>C)s(e,n,l,z+1,r,o,i);else{var E,j,P=o,A=z-l+1,N=new Array(A),O=0,$=0,T=2147483647,I=0;for($=0;$=l;$--)if(null==E&&(E=h(t,a,C+1)),null!=(k=n[$])){var R=E[k.key];null!=R&&(T=R0&&(r[i]=o[t-1]),o[t]=i)}}t=o.length,n=o[t-1];for(;t-- >0;)o[t]=n,n=r[n];return o}(N)).length-1,$=z;$>=l;$--)p=n[$],-1===N[$-l]?f(e,p,r,i,o):j[O]===$-l?O--:g(e,m(p),o),null!=p.dom&&(o=n[$].dom);else for($=z;$>=l;$--)p=n[$],-1===N[$-l]&&f(e,p,r,i,o),null!=p.dom&&(o=n[$].dom)}}else{var L=t.lengthL&&x(t,l,t.length),n.length>L&&s(e,n,l,n.length,r,o,i)}}}function v(e,n,r,o,l,u){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate){var n=a.call(e.attrs.onbeforeupdate,e,t);if(void 0!==n&&!n)break}if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate){var n=a.call(e.state.onbeforeupdate,e,t);if(void 0!==n&&!n)break}return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&T(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(m(t),d(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(e,n,r,u,l);break;case"[":!function(e,t,n,r,o,i){p(e,t.children,n.children,r,o,i);var l=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u0){for(var o=e.dom;--t;)n.appendChild(o.nextSibling);n.insertBefore(o,n.firstChild)}return n}return e.dom}function y(e,t,n){for(;t-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var E=/[A-Z]/g;function j(e){return"-"+e.toLowerCase()}function P(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(E,j)}function A(e,t,n){if(t===n);else if(null==n)e.style.cssText="";else if("object"!=typeof n)e.style.cssText=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(P(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(P(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(P(r))}}function N(){}function O(e,t,n){if(null!=e.events){if(e.events[t]===n)return;null==n||"function"!=typeof n&&"object"!=typeof n?(null!=e.events[t]&&e.dom.removeEventListener(t.slice(2),e.events,!1),e.events[t]=void 0):(null==e.events[t]&&e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=n)}else null==n||"function"!=typeof n&&"object"!=typeof n||(e.events=new N,e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=n)}function $(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function T(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return N.prototype=Object.create(null),N.prototype.handleEvent=function(e){var t,r=this["on"+e.type];"function"==typeof r?t=r.call(e.currentTarget,e):"function"==typeof r.handleEvent&&r.handleEvent(e),!1===e.redraw?e.redraw=void 0:"function"==typeof n&&n(),!1===t&&(e.preventDefault(),e.stopPropagation())},{render:function(e,n){if(!e)throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var r=[],o=u(),i=e.namespaceURI;null==e.vnodes&&(e.textContent=""),n=t.normalizeChildren(Array.isArray(n)?n:[n]),p(e,e.vnodes,n,r,null,"http://www.w3.org/1999/xhtml"===i?void 0:i),e.vnodes=n,null!=o&&u()!==o&&"function"==typeof o.focus&&o.focus();for(var l=0;l-1&&r.splice(t,2)}function l(){if(o)throw new Error("Nested m.redraw.sync() call");o=!0;for(var e=1;e-1&&u.pop();for(var f=0;f-1?r:o>-1?o:e.length;if(r>-1){var l=o>-1?o:e.length,a=b(e.slice(r+1,l));for(var u in a)t[u]=a[u]}if(o>-1){var s=b(e.slice(o+1));for(var u in s)n[u]=s[u]}return e.slice(0,i)}var l={prefix:"#!",getPath:function(){switch(l.prefix.charAt(0)){case"#":return o("hash").slice(l.prefix.length);case"?":return o("search").slice(l.prefix.length)+o("hash");default:return o("pathname").slice(l.prefix.length)+o("search")+o("hash")}},setPath:function(t,r,o){var a={},u={};if(t=i(t,a,u),null!=r){for(var s in r)a[s]=r[s];t=t.replace(/:([^\/]+)/g,function(e,t){return delete a[t],r[t]})}var f=d(a);f&&(t+="?"+f);var c=d(u);if(c&&(t+="#"+c),n){var p=o?o.state:null,v=o?o.title:null;e.onpopstate(),o&&o.replace?e.history.replaceState(p,v,l.prefix+t):e.history.pushState(p,v,l.prefix+t)}else e.location.href=l.prefix+t}};return l.defineRoutes=function(o,a,u){function s(){var t=l.getPath(),n={},r=i(t,n,n),s=e.history.state;if(null!=s)for(var f in s)n[f]=s[f];for(var c in o){var d=new RegExp("^"+c.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$");if(d.test(r))return void r.replace(d,function(){for(var e=c.match(/:[^\/]+/g)||[],r=[].slice.call(arguments,1,-2),i=0;i0&&a[a.length-1])&&(6===i[0]||2===i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]li{white-space:nowrap}.mtc span.mtc__item-title{margin-right:1rem}.mtc .mtc__draggable{cursor:move;cursor:grab;cursor:-webkit-grab}.mtc .mtc__draggable:active{cursor:grabbing;cursor:-webkit-grabbing}.mtc .mtc__as_child{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,0)),color-stop(50%,rgba(41,137,216,.49)),color-stop(51%,rgba(32,124,202,.5)),to(rgba(125,185,232,0)));background:linear-gradient(180deg,rgba(30,87,153,0) 0,rgba(41,137,216,.49) 50%,rgba(32,124,202,.5) 51%,rgba(125,185,232,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(0, 30, 87, 0.6)",endColorstr="rgba(0, 125, 185, 0.9098)",GradientType=0)}.mtc .mtc__below{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,0)),to(rgba(30,87,153,.7)));background:linear-gradient(180deg,rgba(30,87,153,0) 0,rgba(30,87,153,.7));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(0, 30, 87, 0.6)",endColorstr="rgba(179, 30, 87, 0.6)",GradientType=0)}.mtc .mtc__above{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,.7)),to(rgba(30,87,153,0)));background:linear-gradient(180deg,rgba(30,87,153,.7) 0,rgba(30,87,153,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(179, 30, 87, 0.6)",endColorstr="rgba(0, 30, 87, 0.6)",GradientType=0)}');var c="tree-item-",l=function(){var t={isOpen:!1};return{oninit:function(e){var r=e.attrs.options,n=r.isOpen,a=r._hasChildren;t.toggle=function(e){a(e)&&(n?e[n]=!e[n]:t.isOpen=!t.isOpen)},t.open=function(e,r){r||(n?e[n]=!0:t.isOpen||(t.isOpen=!0))}},view:function(r){var n=r.attrs,a=n.item,i=n.options,c=n.dragOptions,d=n.selectedId,u=n.width,s=i.id,f=i.treeItemView,m=i._findChildren,p=i._isExpanded,_=i._addChildren,g=i._hasChildren,b=i._depth,v=i.onSelect,h=i.onDelete,x=i.editable,y=x.canUpdate,w=x.canCreate,I=x.canDelete,k=x.canDeleteParent,O=i.maxDepth,T=t.toggle,C=t.open,D=t.isOpen,S=p(a,D),E=g(a),N=b(a);return(0,e.default)("li"+(y?".mtc__draggable":"")+"[id=tree-item-"+a[s]+"][draggable="+y+"]",c,[(0,e.default)(".mtc__item",{onclick:function(e){e.stopPropagation(),v(a,d!==a[s])}},[(0,e.default)(".mtc__header.mtc__clearfix",{class:d===a[s]?"active":""},[E?(0,e.default)(o,{buttonName:S?"expand_less":"expand_more",onclick:function(){return T(a)}}):void 0,(0,e.default)(".mtc__item-title",{class:(y?"mtc__moveable":"")+" "+(E?"":"mtc__childless-item"),style:"max-width: "+u+"px"},(0,e.default)(f,{treeItem:a,depth:N,width:u}))],(0,e.default)(".mtc__act-group",[!I||!k&&E?"":(0,e.default)(o,{buttonName:"delete",onclick:function(){return h(a)}}),w&&N0&&(n.className=s.join(" ")),l[r]={tag:t,attrs:n}}function i(a,e){var l=e.attrs,n=r.normalizeChildren(e.children),i=t.call(l,"class"),c=i?l.class:l.className;if(e.tag=a.tag,e.attrs=null,e.children=void 0,!s(a.attrs)&&!s(l)){var o={};for(var u in l)t.call(l,u)&&(o[u]=l[u]);l=o}for(var u in a.attrs)t.call(a.attrs,u)&&"className"!==u&&!t.call(l,u)&&(l[u]=a.attrs[u]);for(var u in null==c&&null==a.attrs.className||(l.className=null!=c?null!=a.attrs.className?String(a.attrs.className)+" "+String(c):c:null!=a.attrs.className?a.attrs.className:null),i&&(l.class=null),l)if(t.call(l,u)&&"key"!==u){e.attrs=l;break}return Array.isArray(n)&&1===n.length&&null!=n[0]&&"#"===n[0].tag?e.text=n[0].children:e.children=n,e}function c(e){if(null==e||"string"!=typeof e&&"function"!=typeof e&&"function"!=typeof e.view)throw Error("The selector must be either a string or a component.");var t=a.apply(1,arguments);return"string"==typeof e&&(t.children=r.normalizeChildren(t.children),"["!==e)?i(l[e]||n(e),t):(t.tag=e,t)}module.exports=c; 9 | },{"../render/vnode":"y51D","./hyperscriptVnode":"skWM"}],"b5QZ":[function(require,module,exports) { 10 | "use strict";var e=require("../render/vnode");module.exports=function(r){return null==r&&(r=""),e("<",void 0,void 0,r,void 0,void 0)}; 11 | },{"../render/vnode":"y51D"}],"HdGc":[function(require,module,exports) { 12 | "use strict";var r=require("../render/vnode"),e=require("./hyperscriptVnode");module.exports=function(){var n=e.apply(0,arguments);return n.tag="[",n.children=r.normalizeChildren(n.children),n}; 13 | },{"../render/vnode":"y51D","./hyperscriptVnode":"skWM"}],"R2AL":[function(require,module,exports) { 14 | "use strict";var r=require("./render/hyperscript");r.trust=require("./render/trust"),r.fragment=require("./render/fragment"),module.exports=r; 15 | },{"./render/hyperscript":"l8ze","./render/trust":"b5QZ","./render/fragment":"HdGc"}],"EYVZ":[function(require,module,exports) { 16 | "use strict";var n=function(t){if(!(this instanceof n))throw new Error("Promise must be called with `new`");if("function"!=typeof t)throw new TypeError("executor must be a function");var e=this,r=[],o=[],i=s(r,!0),c=s(o,!1),u=e._instance={resolvers:r,rejectors:o},f="function"==typeof setImmediate?setImmediate:setTimeout;function s(n,t){return function i(s){var h;try{if(!t||null==s||"object"!=typeof s&&"function"!=typeof s||"function"!=typeof(h=s.then))f(function(){t||0!==n.length||console.error("Possible unhandled promise rejection:",s);for(var e=0;e0||n(e)}}var r=e(c);try{n(e(i),r)}catch(o){r(o)}}l(t)};n.prototype.then=function(t,e){var r,o,i=this._instance;function c(n,t,e,c){t.push(function(t){if("function"!=typeof n)e(t);else try{r(n(t))}catch(i){o&&o(i)}}),"function"==typeof i.retry&&c===i.state&&i.retry()}var u=new n(function(n,t){r=n,o=t});return c(t,i.resolvers,r,!0),c(e,i.rejectors,o,!1),u},n.prototype.catch=function(n){return this.then(null,n)},n.prototype.finally=function(t){return this.then(function(e){return n.resolve(t()).then(function(){return e})},function(e){return n.resolve(t()).then(function(){return n.reject(e)})})},n.resolve=function(t){return t instanceof n?t:new n(function(n){n(t)})},n.reject=function(t){return new n(function(n,e){e(t)})},n.all=function(t){return new n(function(n,e){var r=t.length,o=0,i=[];if(0===t.length)n([]);else for(var c=0;c'+t.children+"",i=i.firstChild):i.innerHTML=t.children,t.dom=i.firstChild,t.domSize=i.childNodes.length,t.instance=[];for(var a,u=l.createDocumentFragment();a=i.firstChild;)t.instance.push(a),u.appendChild(a);b(e,u,o)}function v(e,t,n,l,o,r){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)s(e,n,0,n.length,l,o,r);else if(null==n||0===n.length)x(e,t,0,t.length);else{var i=null!=t[0]&&null!=t[0].key,a=null!=n[0]&&null!=n[0].key,u=0,d=0;if(!i)for(;d=d&&$>=u&&(b=t[E],w=n[$],b.key===w.key);)b!==w&&m(e,b,w,l,o,r),null!=w.dom&&(o=w.dom),E--,$--;for(;E>=d&&$>=u&&(c=t[d],v=n[u],c.key===v.key);)d++,u++,c!==v&&m(e,c,v,l,p(t,d,o),r);for(;E>=d&&$>=u&&u!==$&&c.key===w.key&&b.key===v.key;)y(e,b,S=p(t,d,o)),b!==v&&m(e,b,v,l,S,r),++u<=--$&&y(e,c,o),c!==w&&m(e,c,w,l,o,r),null!=w.dom&&(o=w.dom),d++,b=t[--E],w=n[$],c=t[d],v=n[u];for(;E>=d&&$>=u&&b.key===w.key;)b!==w&&m(e,b,w,l,o,r),null!=w.dom&&(o=w.dom),$--,b=t[--E],w=n[$];if(u>$)x(e,t,d,E+1);else if(d>E)s(e,n,u,$+1,l,o,r);else{var z,C,A=o,L=$-u+1,N=new Array(L),T=0,j=0,I=2147483647,M=0;for(j=0;j=u;j--){null==z&&(z=h(t,d,E+1));var O=z[(w=n[j]).key];null!=O&&(I=O>>1)+(l>>>1)+(n&l&1);e[t[a]]0&&(g[o]=t[n-1]),t[n]=o)}}n=t.length,l=t[n-1];for(;n-- >0;)t[n]=l,l=g[l];return g.length=0,t}(N)).length-1,j=$;j>=u;j--)v=n[j],-1===N[j-u]?f(e,v,l,r,o):C[T]===j-u?T--:y(e,v,o),null!=v.dom&&(o=n[j].dom);else for(j=$;j>=u;j--)v=n[j],-1===N[j-u]&&f(e,v,l,r,o),null!=v.dom&&(o=n[j].dom)}}else{var D=t.lengthD&&x(e,t,u,t.length),n.length>D&&s(e,n,u,n.length,l,o,r)}}}function m(t,n,l,o,i,u){var s=n.tag;if(s===l.tag){if(l.state=n.state,l.events=n.events,function(e,t){do{if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate){var n=a.call(e.attrs.onbeforeupdate,e,t);if(void 0!==n&&!n)break}if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate){var n=a.call(e.state.onbeforeupdate,e,t);if(void 0!==n&&!n)break}return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,e.text=t.text,!0}(l,n))return;if("string"==typeof s)switch(null!=l.attrs&&F(l.attrs,l,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,l);break;case"<":!function(e,t,n,l,o){t.children!==n.children?(S(e,t),c(e,n,l,o)):(n.dom=t.dom,n.domSize=t.domSize,n.instance=t.instance)}(t,n,l,u,i);break;case"[":!function(e,t,n,l,o,r){v(e,t.children,n.children,l,o,r);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var N=/[A-Z]/g;function T(e){return"-"+e.toLowerCase()}function j(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(N,T)}function I(e,t,n){if(t===n);else if(null==n)e.style.cssText="";else if("object"!=typeof n)e.style.cssText=n;else if(null==t||"object"!=typeof t)for(var l in e.style.cssText="",n){null!=(o=n[l])&&e.style.setProperty(j(l),String(o))}else{for(var l in n){var o;null!=(o=n[l])&&(o=String(o))!==String(t[l])&&e.style.setProperty(j(l),o)}for(var l in t)null!=t[l]&&null==n[l]&&e.style.removeProperty(j(l))}}function M(){this._=n}function O(e,t,n){if(null!=e.events){if(e.events[t]===n)return;null==n||"function"!=typeof n&&"object"!=typeof n?(null!=e.events[t]&&e.dom.removeEventListener(t.slice(2),e.events,!1),e.events[t]=void 0):(null==e.events[t]&&e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=n)}else null==n||"function"!=typeof n&&"object"!=typeof n||(e.events=new M,e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=n)}function D(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function F(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return M.prototype=Object.create(null),M.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(t,l,o){if(!t)throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var r=[],i=u(),a=t.namespaceURI;null==t.vnodes&&(t.textContent=""),l=e.normalizeChildren(Array.isArray(l)?l:[l]);var s=n;try{n="function"==typeof o?o:void 0,v(t,t.vnodes,l,r,null,"http://www.w3.org/1999/xhtml"===a?void 0:a)}finally{n=s}t.vnodes=l,null!=i&&u()!==i&&"function"==typeof i.focus&&i.focus();for(var f=0;f=0&&(t.splice(c,2),e(r,[],l)),null!=o&&(t.push(r,o),e(r,n(o),l))},redraw:l}}; 26 | },{"../render/vnode":"y51D"}],"JmBI":[function(require,module,exports) { 27 | "use strict";var e=require("./render");module.exports=require("./api/mount-redraw")(e,requestAnimationFrame,console); 28 | },{"./render":"knD4","./api/mount-redraw":"Lt2a"}],"e1pw":[function(require,module,exports) { 29 | "use strict";module.exports=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var o in e)r(o,e[o]);return t.join("&");function r(e,o){if(Array.isArray(o))for(var n=0;n=0&&(p+=n.slice(i,s)),o>=0&&(p+=(i<0?"?":"&")+d.slice(o,g));var x=e(c);return x&&(p+=(i<0&&o<0?"?":"&")+x),l>=0&&(p+=n.slice(l)),f>=0&&(p+=(l<0?"":"&")+d.slice(f)),p}; 34 | },{"../querystring/build":"e1pw","./assign":"TIRB"}],"Nv5j":[function(require,module,exports) { 35 | "use strict";var e=require("../pathname/build");module.exports=function(t,n,r){var o=0;function a(e){return new n(e)}function i(t){return function(o,i){"string"!=typeof o?(i=o,o=o.url):null==i&&(i={});var s=new n(function(n,r){t(e(o,i.params),i,function(e){if("function"==typeof i.type)if(Array.isArray(e))for(var t=0;t=200&&t.target.status<300||304===t.target.status||/^file:\/\//i.test(e),s=t.target.response;if("json"===p?t.target.responseType||"function"==typeof n.extract||(s=JSON.parse(t.target.responseText)):p&&"text"!==p||null==s&&(s=t.target.responseText),"function"==typeof n.extract?(s=n.extract(t.target,n),i=!0):"function"==typeof n.deserialize&&(s=n.deserialize(s)),i)r(s);else{try{a=t.target.responseText}catch(u){a=s}var c=new Error(a);c.code=t.target.status,c.response=s,o(c)}}catch(u){o(u)}},"function"==typeof n.config&&(f=n.config(f,n,e)||f)!==d&&(a=f.abort,f.abort=function(){l=!0,a.call(this)}),null==c?f.send():"function"==typeof n.serialize?f.send(n.serialize(c)):c instanceof t.FormData?f.send(c):f.send(JSON.stringify(c))}),jsonp:i(function(e,n,r,a){var i=n.callbackName||"_mithril_"+Math.round(1e16*Math.random())+"_"+o++,s=t.document.createElement("script");t[i]=function(e){delete t[i],s.parentNode.removeChild(s),r(e)},s.onerror=function(){delete t[i],s.parentNode.removeChild(s),a(new Error("JSONP request failed"))},s.src=e+(e.indexOf("?")<0?"?":"&")+encodeURIComponent(n.callbackKey||"callback")+"="+encodeURIComponent(i),t.document.documentElement.appendChild(s)})}}; 36 | },{"../pathname/build":"bE7V"}],"JA2T":[function(require,module,exports) { 37 | "use strict";var e=require("./promise/promise"),r=require("./mount-redraw");module.exports=require("./request/request")(window,e,r.redraw); 38 | },{"./promise/promise":"PcbK","./mount-redraw":"JmBI","./request/request":"Nv5j"}],"Pflx":[function(require,module,exports) { 39 | "use strict";module.exports=function(e){if(""===e||null==e)return{};"?"===e.charAt(0)&&(e=e.slice(1));for(var r=e.split("&"),t={},l={},n=0;n-1&&a.pop();for(var u=0;u1&&"/"===l[l.length-1]&&(l=l.slice(0,-1))):l="/",{path:l,params:t<0?{}:e(r.slice(t+1,n))}}; 42 | },{"../querystring/parse":"Pflx"}],"LcIX":[function(require,module,exports) { 43 | "use strict";var r=require("./parse");module.exports=function(e){var t=r(e),n=Object.keys(t.params),a=[],u=new RegExp("^"+t.path.replace(/:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g,function(r,e,t){return null==e?"\\"+r:(a.push({k:e,r:"..."===t}),"..."===t?"(.*)":"."===t?"([^/]+)\\.":"([^/]+)"+(t||""))})+"$");return function(r){for(var e=0;e0&&a[a.length-1])||6!==o[0]&&2!==o[0])){i=0;continue}if(3===o[0]&&(!a||o[1]>a[0]&&o[1]li{white-space:nowrap}.mtc span.mtc__item-title{margin-right:1rem}.mtc .mtc__draggable{cursor:move;cursor:grab;cursor:-webkit-grab}.mtc .mtc__draggable:active{cursor:grabbing;cursor:-webkit-grabbing}.mtc .mtc__as_child{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,0)),color-stop(50%,rgba(41,137,216,.49)),color-stop(51%,rgba(32,124,202,.5)),to(rgba(125,185,232,0)));background:linear-gradient(180deg,rgba(30,87,153,0) 0,rgba(41,137,216,.49) 50%,rgba(32,124,202,.5) 51%,rgba(125,185,232,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(0, 30, 87, 0.6)",endColorstr="rgba(0, 125, 185, 0.9098)",GradientType=0)}.mtc .mtc__below{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,0)),to(rgba(30,87,153,.7)));background:linear-gradient(180deg,rgba(30,87,153,0) 0,rgba(30,87,153,.7));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(0, 30, 87, 0.6)",endColorstr="rgba(179, 30, 87, 0.6)",GradientType=0)}.mtc .mtc__above{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,.7)),to(rgba(30,87,153,0)));background:linear-gradient(180deg,rgba(30,87,153,.7) 0,rgba(30,87,153,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(179, 30, 87, 0.6)",endColorstr="rgba(0, 30, 87, 0.6)",GradientType=0)}');var d="tree-item-",u=function(){var t={};return{oninit:function(e){var r=e.attrs.options,n=r.isOpen,a=void 0===n?"isOpen":n,o=r.id,i=r._hasChildren;t.toggle=function(e,t){i(e)&&("function"==typeof a?a(e[o],"set",!a(e[o],"get")):e[a]=!e[a],t&&t(e,"function"==typeof a?a(e[o],"get"):e[a]))},t.open=function(e,t){t||("function"==typeof a?a(e[o],"set",!0):e[a]=!0)}},view:function(r){var n=r.attrs,a=n.item,i=n.options,c=n.dragOptions,d=n.selectedId,s=n.width,f=i.id,m=i.treeItemView,p=i._findChildren,g=i._isExpanded,_=i._addChildren,b=i._hasChildren,v=i._depth,h=i.onSelect,x=i.onToggle,y=i.onDelete,w=i.editable,I=w.canUpdate,k=w.canCreate,T=w.canDelete,O=w.canDeleteParent,C=i.maxDepth,D=t.toggle,S=t.open,E=g(a),P=b(a),N=v(a);return(0,e.default)("li"+(I?".mtc__draggable":"")+"[id=tree-item-"+a[f]+"][draggable="+I+"]",c,[(0,e.default)(".mtc__item",{onclick:function(e){e.stopPropagation(),h(a,d!==a[f])}},[(0,e.default)(".mtc__header.mtc__clearfix",{class:d===a[f]?"active":""},[P?(0,e.default)(l,{buttonName:E?"expand_less":"expand_more",onclick:function(){return D(a,x)}}):void 0,(0,e.default)(".mtc__item-title",{class:(I?"mtc__moveable":"")+" "+(P?"":"mtc__childless-item"),style:"max-width: "+s+"px"},(0,e.default)(m,{treeItem:a,depth:N,width:s}))],(0,e.default)(".mtc__act-group",[!T||!O&&P?"":(0,e.default)(l,{buttonName:"delete",onclick:function(){return y(a)}}),k&&N0&&(l.className=i.join(" ")),o[e]={tag:n,attrs:l}}function u(e,n){var r=n.attrs,o=t.normalizeChildren(n.children),a=i.call(r,"class"),u=a?r.class:r.className;if(n.tag=e.tag,n.attrs=null,n.children=void 0,!l(e.attrs)&&!l(r)){var s={};for(var f in r)i.call(r,f)&&(s[f]=r[f]);r=s}for(var f in e.attrs)i.call(e.attrs,f)&&"className"!==f&&!i.call(r,f)&&(r[f]=e.attrs[f]);for(var f in null==u&&null==e.attrs.className||(r.className=null!=u?null!=e.attrs.className?String(e.attrs.className)+" "+String(u):u:null!=e.attrs.className?e.attrs.className:null),a&&(r.class=null),r)if(i.call(r,f)&&"key"!==f){n.attrs=r;break}return Array.isArray(o)&&1===o.length&&null!=o[0]&&"#"===o[0].tag?n.text=o[0].children:n.children=o,n}function s(e){if(null==e||"string"!=typeof e&&"function"!=typeof e&&"function"!=typeof e.view)throw Error("The selector must be either a string or a component.");var r=n.apply(1,arguments);return"string"==typeof e&&(r.children=t.normalizeChildren(r.children),"["!==e)?u(o[e]||a(e),r):(r.tag=e,r)}s.trust=function(e){return null==e&&(e=""),t("<",void 0,void 0,e,void 0,void 0)},s.fragment=function(){var e=n.apply(0,arguments);return e.tag="[",e.children=t.normalizeChildren(e.children),e};var f=function(){return s.apply(this,arguments)};if(f.m=s,f.trust=s.trust,f.fragment=s.fragment,(c=function(e){if(!(this instanceof c))throw new Error("Promise must be called with `new`");if("function"!=typeof e)throw new TypeError("executor must be a function");var t=this,n=[],r=[],o=u(n,!0),i=u(r,!1),l=t._instance={resolvers:n,rejectors:r},a="function"==typeof setImmediate?setImmediate:setTimeout;function u(e,o){return function u(f){var c;try{if(!o||null==f||"object"!=typeof f&&"function"!=typeof f||"function"!=typeof(c=f.then))a(function(){o||0!==e.length||console.error("Possible unhandled promise rejection:",f);for(var t=0;t0||e(n)}}var r=n(i);try{e(n(o),r)}catch(l){r(l)}}s(e)}).prototype.then=function(e,t){var n,r,o=this._instance;function i(e,t,i,l){t.push(function(t){if("function"!=typeof e)i(t);else try{n(e(t))}catch(o){r&&r(o)}}),"function"==typeof o.retry&&l===o.state&&o.retry()}var l=new c(function(e,t){n=e,r=t});return i(e,o.resolvers,n,!0),i(t,o.rejectors,r,!1),l},c.prototype.catch=function(e){return this.then(null,e)},c.prototype.finally=function(e){return this.then(function(t){return c.resolve(e()).then(function(){return t})},function(t){return c.resolve(e()).then(function(){return c.reject(t)})})},c.resolve=function(e){return e instanceof c?e:new c(function(t){t(e)})},c.reject=function(e){return new c(function(t,n){n(e)})},c.all=function(e){return new c(function(t,n){var r=e.length,o=0,i=[];if(0===e.length)t([]);else for(var l=0;l=200&&c.status<300||304===c.status||/^file:\/\//i.test(t),i=c.responseText;if("function"==typeof n.extract)i=n.extract(c,n),e=!0;else if("function"==typeof n.deserialize)i=n.deserialize(i);else try{i=i?JSON.parse(i):null}catch(a){throw new Error("Invalid JSON: "+i)}if(e)r(i);else{var l=new Error(c.responseText);l.code=c.status,l.response=i,o(l)}}catch(a){o(a)}},u&&null!=s?c.send(s):c.send()}),jsonp:o(function(t,n,o,i){var a=n.callbackName||"_mithril_"+Math.round(1e16*Math.random())+"_"+r++,u=e.document.createElement("script");e[a]=function(t){u.parentNode.removeChild(u),o(t),delete e[a]},u.onerror=function(){u.parentNode.removeChild(u),i(new Error("JSONP request failed")),delete e[a]},t=l(t,n.data,!0),u.src=t+(t.indexOf("?")<0?"?":"&")+encodeURIComponent(n.callbackKey||"callback")+"="+encodeURIComponent(a),e.document.documentElement.appendChild(u)}),setCompletionCallback:function(e){n=e}}},v=p(window,c),h=function(e){var n,r=e.document,o={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function i(e){return e.attrs&&e.attrs.xmlns||o[e.tag]}function l(e,t){if(e.state!==t)throw new Error("`vnode.state` must not be modified")}function a(e){var t=e.state;try{return this.apply(t,arguments)}finally{l(e,t)}}function u(){try{return r.activeElement}catch(e){return null}}function s(e,t,n,r,o,i,l){for(var a=n;a'+t.children+"",l=l.firstChild):l.innerHTML=t.children,t.dom=l.firstChild,t.domSize=l.childNodes.length;for(var a,u=r.createDocumentFragment();a=l.firstChild;)u.appendChild(a);g(e,u,o)}function p(e,t,n,r,o,i){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)s(e,n,0,n.length,r,o,i);else if(null==n||0===n.length)x(t,0,t.length);else{for(var l=0,a=0,u=null,c=null;a=a&&z>=l;)if(w=t[C],k=n[z],null==w)C--;else if(null==k)z--;else{if(w.key!==k.key)break;w!==k&&v(e,w,k,r,o,i),null!=k.dom&&(o=k.dom),C--,z--}for(;C>=a&&z>=l;)if(d=t[a],p=n[l],null==d)a++;else if(null==p)l++;else{if(d.key!==p.key)break;a++,l++,d!==p&&v(e,d,p,r,y(t,a,o),i)}for(;C>=a&&z>=l;){if(null==d)a++;else if(null==p)l++;else if(null==w)C--;else if(null==k)z--;else{if(l===z)break;if(d.key!==k.key||w.key!==p.key)break;S=y(t,a,o),g(e,m(w),S),w!==p&&v(e,w,p,r,S,i),++l<=--z&&g(e,m(d),o),d!==k&&v(e,d,k,r,o,i),null!=k.dom&&(o=k.dom),a++,C--}w=t[C],k=n[z],d=t[a],p=n[l]}for(;C>=a&&z>=l;){if(null==w)C--;else if(null==k)z--;else{if(w.key!==k.key)break;w!==k&&v(e,w,k,r,o,i),null!=k.dom&&(o=k.dom),C--,z--}w=t[C],k=n[z]}if(l>z)x(t,a,C+1);else if(a>C)s(e,n,l,z+1,r,o,i);else{var E,j,P=o,A=z-l+1,N=new Array(A),O=0,$=0,T=2147483647,I=0;for($=0;$=l;$--)if(null==E&&(E=h(t,a,C+1)),null!=(k=n[$])){var R=E[k.key];null!=R&&(T=R0&&(r[i]=o[t-1]),o[t]=i)}}t=o.length,n=o[t-1];for(;t-- >0;)o[t]=n,n=r[n];return o}(N)).length-1,$=z;$>=l;$--)p=n[$],-1===N[$-l]?f(e,p,r,i,o):j[O]===$-l?O--:g(e,m(p),o),null!=p.dom&&(o=n[$].dom);else for($=z;$>=l;$--)p=n[$],-1===N[$-l]&&f(e,p,r,i,o),null!=p.dom&&(o=n[$].dom)}}else{var L=t.lengthL&&x(t,l,t.length),n.length>L&&s(e,n,l,n.length,r,o,i)}}}function v(e,n,r,o,l,u){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate){var n=a.call(e.attrs.onbeforeupdate,e,t);if(void 0!==n&&!n)break}if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate){var n=a.call(e.state.onbeforeupdate,e,t);if(void 0!==n&&!n)break}return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&T(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(m(t),d(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(e,n,r,u,l);break;case"[":!function(e,t,n,r,o,i){p(e,t.children,n.children,r,o,i);var l=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u0){for(var o=e.dom;--t;)n.appendChild(o.nextSibling);n.insertBefore(o,n.firstChild)}return n}return e.dom}function y(e,t,n){for(;t-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var E=/[A-Z]/g;function j(e){return"-"+e.toLowerCase()}function P(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(E,j)}function A(e,t,n){if(t===n);else if(null==n)e.style.cssText="";else if("object"!=typeof n)e.style.cssText=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(P(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(P(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(P(r))}}function N(){}function O(e,t,n){if(null!=e.events){if(e.events[t]===n)return;null==n||"function"!=typeof n&&"object"!=typeof n?(null!=e.events[t]&&e.dom.removeEventListener(t.slice(2),e.events,!1),e.events[t]=void 0):(null==e.events[t]&&e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=n)}else null==n||"function"!=typeof n&&"object"!=typeof n||(e.events=new N,e.dom.addEventListener(t.slice(2),e.events,!1),e.events[t]=n)}function $(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function T(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return N.prototype=Object.create(null),N.prototype.handleEvent=function(e){var t,r=this["on"+e.type];"function"==typeof r?t=r.call(e.currentTarget,e):"function"==typeof r.handleEvent&&r.handleEvent(e),!1===e.redraw?e.redraw=void 0:"function"==typeof n&&n(),!1===t&&(e.preventDefault(),e.stopPropagation())},{render:function(e,n){if(!e)throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var r=[],o=u(),i=e.namespaceURI;null==e.vnodes&&(e.textContent=""),n=t.normalizeChildren(Array.isArray(n)?n:[n]),p(e,e.vnodes,n,r,null,"http://www.w3.org/1999/xhtml"===i?void 0:i),e.vnodes=n,null!=o&&u()!==o&&"function"==typeof o.focus&&o.focus();for(var l=0;l-1&&r.splice(t,2)}function l(){if(o)throw new Error("Nested m.redraw.sync() call");o=!0;for(var e=1;e-1&&u.pop();for(var f=0;f-1?r:o>-1?o:e.length;if(r>-1){var l=o>-1?o:e.length,a=b(e.slice(r+1,l));for(var u in a)t[u]=a[u]}if(o>-1){var s=b(e.slice(o+1));for(var u in s)n[u]=s[u]}return e.slice(0,i)}var l={prefix:"#!",getPath:function(){switch(l.prefix.charAt(0)){case"#":return o("hash").slice(l.prefix.length);case"?":return o("search").slice(l.prefix.length)+o("hash");default:return o("pathname").slice(l.prefix.length)+o("search")+o("hash")}},setPath:function(t,r,o){var a={},u={};if(t=i(t,a,u),null!=r){for(var s in r)a[s]=r[s];t=t.replace(/:([^\/]+)/g,function(e,t){return delete a[t],r[t]})}var f=d(a);f&&(t+="?"+f);var c=d(u);if(c&&(t+="#"+c),n){var p=o?o.state:null,v=o?o.title:null;e.onpopstate(),o&&o.replace?e.history.replaceState(p,v,l.prefix+t):e.history.pushState(p,v,l.prefix+t)}else e.location.href=l.prefix+t}};return l.defineRoutes=function(o,a,u){function s(){var t=l.getPath(),n={},r=i(t,n,n),s=e.history.state;if(null!=s)for(var f in s)n[f]=s[f];for(var c in o){var d=new RegExp("^"+c.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$");if(d.test(r))return void r.replace(d,function(){for(var e=c.match(/:[^\/]+/g)||[],r=[].slice.call(arguments,1,-2),i=0;i0&&a[a.length-1])&&(6===i[0]||2===i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]li{white-space:nowrap}.mtc span.mtc__item-title{margin-right:1rem}.mtc .mtc__draggable{cursor:move;cursor:grab;cursor:-webkit-grab}.mtc .mtc__draggable:active{cursor:grabbing;cursor:-webkit-grabbing}.mtc .mtc__as_child{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,0)),color-stop(50%,rgba(41,137,216,.49)),color-stop(51%,rgba(32,124,202,.5)),to(rgba(125,185,232,0)));background:linear-gradient(180deg,rgba(30,87,153,0) 0,rgba(41,137,216,.49) 50%,rgba(32,124,202,.5) 51%,rgba(125,185,232,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(0, 30, 87, 0.6)",endColorstr="rgba(0, 125, 185, 0.9098)",GradientType=0)}.mtc .mtc__below{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,0)),to(rgba(30,87,153,.7)));background:linear-gradient(180deg,rgba(30,87,153,0) 0,rgba(30,87,153,.7));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(0, 30, 87, 0.6)",endColorstr="rgba(179, 30, 87, 0.6)",GradientType=0)}.mtc .mtc__above{background:-webkit-gradient(linear,left top,left bottom,from(rgba(30,87,153,.7)),to(rgba(30,87,153,0)));background:linear-gradient(180deg,rgba(30,87,153,.7) 0,rgba(30,87,153,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="rgba(179, 30, 87, 0.6)",endColorstr="rgba(0, 30, 87, 0.6)",GradientType=0)}');var c="tree-item-",l=function(){var t={isOpen:!1};return{oninit:function(e){var r=e.attrs.options,n=r.isOpen,a=r._hasChildren;t.toggle=function(e){a(e)&&(n?e[n]=!e[n]:t.isOpen=!t.isOpen)},t.open=function(e,r){r||(n?e[n]=!0:t.isOpen||(t.isOpen=!0))}},view:function(r){var n=r.attrs,a=n.item,i=n.options,c=n.dragOptions,d=n.selectedId,u=n.width,s=i.id,f=i.treeItemView,m=i._findChildren,p=i._isExpanded,_=i._addChildren,g=i._hasChildren,b=i._depth,v=i.onSelect,h=i.onDelete,x=i.editable,y=x.canUpdate,w=x.canCreate,I=x.canDelete,k=x.canDeleteParent,O=i.maxDepth,T=t.toggle,C=t.open,D=t.isOpen,S=p(a,D),E=g(a),N=b(a);return(0,e.default)("li"+(y?".mtc__draggable":"")+"[id=tree-item-"+a[s]+"][draggable="+y+"]",c,[(0,e.default)(".mtc__item",{onclick:function(e){e.stopPropagation(),v(a,d!==a[s])}},[(0,e.default)(".mtc__header.mtc__clearfix",{class:d===a[s]?"active":""},[E?(0,e.default)(o,{buttonName:S?"expand_less":"expand_more",onclick:function(){return T(a)}}):void 0,(0,e.default)(".mtc__item-title",{class:(y?"mtc__moveable":"")+" "+(E?"":"mtc__childless-item"),style:"max-width: "+u+"px"},(0,e.default)(f,{treeItem:a,depth:N,width:u}))],(0,e.default)(".mtc__act-group",[!I||!k&&E?"":(0,e.default)(o,{buttonName:"delete",onclick:function(){return h(a)}}),w&&NMithril-tree-component example -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-tree-component", 3 | "version": "0.0.1", 4 | "description": "A tree component for the Mitrhil framework.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/erikvullings/mithril-tree-component.git" 8 | }, 9 | "keywords": [ 10 | "mithril", 11 | "tree", 12 | "component", 13 | "typescript" 14 | ], 15 | "author": "Erik Vullings (http://www.tno.nl)", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/erikvullings/mithril-tree-component/issues" 19 | }, 20 | "homepage": "https://github.com/erikvullings/mithril-tree-component#readme", 21 | "scripts": { 22 | "postinstall": "lerna run link", 23 | "build": "lerna run build", 24 | "clean:local": "rimraf ./docs", 25 | "clean": "npm run clean:local && lerna run --parallel clean", 26 | "start": "lerna run --parallel start", 27 | "deploy": "lerna run --parallel deploy", 28 | "patch-release": "lerna run patch-release", 29 | "minor-release": "lerna run minor-release" 30 | }, 31 | "devDependencies": { 32 | "lerna": "^3.22.1", 33 | "rimraf": "^3.0.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/example/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = false 10 | 11 | # 2 space indentation 12 | [**.*] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /packages/example/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .cache -------------------------------------------------------------------------------- /packages/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mithril-tree-component example 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-tree-component-example", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Example project, showing how to use the Mitrhil tree component.", 6 | "main": "index.js", 7 | "scripts": { 8 | "clean": "rimraf ./.cache ./dist", 9 | "link": "pnpm link mithril-tree-component", 10 | "start": "parcel index.html", 11 | "build": "parcel build index.html --out-dir ../../docs --public-url https://erikvullings.github.io/mithril-tree-component", 12 | "deploy": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/erikvullings/mithril-tree-component.git" 17 | }, 18 | "keywords": [ 19 | "mithril", 20 | "tree", 21 | "component", 22 | "typescript" 23 | ], 24 | "author": "Erik Vullings (http://www.tno.nl)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/erikvullings/mithril-tree-component/issues" 28 | }, 29 | "homepage": "https://github.com/erikvullings/mithril-tree-component#readme", 30 | "dependencies": { 31 | "materialize-css": "^1.0.0", 32 | "mithril": "^2.0.4", 33 | "mithril-tree-component": "^0.7.0" 34 | }, 35 | "devDependencies": { 36 | "@types/mithril": "github:MithrilJS/mithril.d.ts#v2", 37 | "autoprefixer": "^10.0.1", 38 | "cssnano": "^4.1.10", 39 | "parcel-bundler": "^1.12.4", 40 | "rimraf": "^3.0.2", 41 | "typescript": "^4.0.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/example/src/app.ts: -------------------------------------------------------------------------------- 1 | import 'materialize-css/dist/css/materialize.min.css'; 2 | import './styles.css'; 3 | import m, { RouteDefs } from 'mithril'; 4 | import { TreeView } from './components/tree-view'; 5 | import { Layout } from './components/layout'; 6 | 7 | const routingTable: RouteDefs = { 8 | '/': { 9 | render: () => m(Layout, m(TreeView)), 10 | }, 11 | }; 12 | 13 | m.route(document.body, '/', routingTable); 14 | -------------------------------------------------------------------------------- /packages/example/src/components/layout.ts: -------------------------------------------------------------------------------- 1 | import m, { Vnode } from 'mithril'; 2 | 3 | export const Layout = () => ({ 4 | view: (vnode: Vnode) => 5 | m('container', [ 6 | m( 7 | 'nav', 8 | m('.nav-wrapper', [ 9 | m( 10 | 'a.brand-logo', 11 | { style: 'margin-left: 20px' }, 12 | m('span', { style: 'margin-top: 10px; margin-left: -10px;' }, 'MITHRIL TREE COMPONENT') 13 | ), 14 | ]) 15 | ), 16 | m('section.main', vnode.children), 17 | ]), 18 | }); 19 | -------------------------------------------------------------------------------- /packages/example/src/components/tree-view.ts: -------------------------------------------------------------------------------- 1 | import m, { Component } from 'mithril'; 2 | import { TreeContainer, ITreeOptions, ITreeItem, uuid4, ITreeItemViewComponent } from 'mithril-tree-component'; 3 | 4 | interface IMyTree extends ITreeItem { 5 | id: number | string; 6 | parentId: number | string; 7 | title: string; 8 | description: string; 9 | } 10 | 11 | const isOpen = (() => { 12 | const store: Record = {}; 13 | return (id: string, action: 'get' | 'set', value?: boolean) => { 14 | if (action === 'get') { 15 | return store.hasOwnProperty(id) ? store[id] : false; 16 | } else if (typeof value !== 'undefined') { 17 | store[id] = value; 18 | } 19 | }; 20 | })(); 21 | 22 | export const TreeView = () => { 23 | let selectedId: string | number = 5; 24 | 25 | const data: IMyTree[] = [ 26 | { 27 | id: 1, 28 | parentId: 0, 29 | title: 'My id is 1', 30 | description: 'Description of item 1.', 31 | }, 32 | { 33 | id: 2, 34 | parentId: 1, 35 | title: 'My id is 2', 36 | description: 'Description of item 2.', 37 | }, 38 | { 39 | id: 3, 40 | parentId: 1, 41 | title: 'My id is 3', 42 | description: 'Description of item 3.', 43 | }, 44 | { 45 | id: 4, 46 | parentId: 2, 47 | title: 'I have a very long title which should be displayed using ellipses if everything works as expected', 48 | description: 'Description of item 4.', 49 | }, 50 | { 51 | id: 5, 52 | parentId: 0, 53 | title: 'My id is 5 - I am not a drop target', 54 | description: 'Items cannot be dropped on me.', 55 | }, 56 | { 57 | id: 6, 58 | parentId: 0, 59 | title: 'My id is 6', 60 | description: 'Description of item 6.', 61 | }, 62 | { 63 | id: 7, 64 | parentId: 0, 65 | title: 'My id is 7', 66 | description: 'Description of item 7.', 67 | }, 68 | { 69 | id: 8, 70 | parentId: 4, 71 | title: 'My id is 8', 72 | description: 'Description of item 8.', 73 | }, 74 | ]; 75 | const emptyTree: IMyTree[] = []; 76 | const tree = data; 77 | const options = { 78 | logging: true, 79 | id: 'id', 80 | parentId: 'parentId', 81 | // isOpen: "isOpen", 82 | isOpen, 83 | name: 'title', 84 | onToggle: (ti, isExpanded) => console.log(`On toggle: "${ti.title}" is ${isExpanded ? '' : 'not '}expanded.`), 85 | onSelect: (ti, isSelected) => { 86 | selectedId = ti.id; 87 | console.log(`On ${isSelected ? 'select' : 'unselect'}: ${ti.title}`); 88 | }, 89 | onBeforeCreate: (ti) => console.log(`On before create ${ti.title}`), 90 | onCreate: (ti) => { 91 | console.log(`On create ${ti.title}`); 92 | // selectedId = ti.id; 93 | }, 94 | onBeforeDelete: (ti) => console.log(`On before delete ${ti.title}`), 95 | onDelete: (ti) => console.log(`On delete ${ti.title}`), 96 | onBeforeUpdate: (ti, action, newParent) => { 97 | console.log(`On before ${action} update ${ti.title} to ${newParent ? newParent.title : ''}.`); 98 | const result = newParent && newParent.id === 5 ? false : true; 99 | console.warn(result ? 'Drop allowed' : 'Drop not allowed'); 100 | return result; 101 | }, 102 | onUpdate: (ti) => console.log(`On update ${ti.title}`), 103 | create: (parent?: IMyTree) => { 104 | const item = {} as IMyTree; 105 | item.id = uuid4(); 106 | if (parent) { 107 | item.parentId = parent.id; 108 | } 109 | item.title = `Created at ${new Date().toLocaleTimeString()}`; 110 | return item as IMyTree; 111 | }, 112 | editable: { 113 | canCreate: false, 114 | canDelete: false, 115 | canUpdate: false, 116 | canDeleteParent: false, 117 | }, 118 | } as Partial; 119 | 120 | const optionsCRUD = { 121 | ...options, 122 | placeholder: 'Create a new tree item', 123 | editable: { 124 | canCreate: true, 125 | canDelete: true, 126 | canUpdate: true, 127 | canDeleteParent: false, 128 | }, 129 | }; 130 | 131 | const optionsOwnView = { 132 | ...options, 133 | maxDepth: 3, 134 | treeItemView: { 135 | view: ({ 136 | attrs: { 137 | depth, 138 | treeItem: { title, description }, 139 | width, 140 | }, 141 | }) => 142 | m( 143 | 'div', 144 | { style: `max-width: ${width - 32}px` }, 145 | m('div', { style: 'font-weight: bold; margin-right: 1rem' }, `Depth ${depth}: ${title}`), 146 | m('div', { style: 'font-style: italic;' }, description || '...') 147 | ), 148 | } as Component, 149 | } as ITreeOptions; 150 | 151 | const optionsCRUDisOpen = { 152 | ...options, 153 | isOpen: undefined, 154 | editable: { 155 | canCreate: true, 156 | canDelete: true, 157 | canUpdate: true, 158 | canDeleteParent: false, 159 | }, 160 | } as ITreeOptions; 161 | 162 | const optionsAsync = { 163 | ...options, 164 | isOpen: undefined, 165 | editable: { 166 | canCreate: true, 167 | canDelete: true, 168 | canUpdate: true, 169 | canDeleteParent: false, 170 | }, 171 | onBeforeDelete: (ti) => { 172 | return new Promise((resolve) => { 173 | setTimeout(() => { 174 | console.log(`On before delete ${ti.title}`); 175 | resolve(true); 176 | }, 3000); 177 | }); 178 | }, 179 | onBeforeCreate: (ti) => { 180 | return new Promise((resolve) => { 181 | setTimeout(() => { 182 | console.log(`On before create ${ti.title}`); 183 | resolve(true); 184 | }, 3000); 185 | }); 186 | }, 187 | onDelete: (ti) => { 188 | console.log(`On delete ${ti.title}`); 189 | m.redraw(); // As the delete action is done async, force a redraw when done. 190 | }, 191 | onCreate: (ti) => { 192 | console.log(`On create ${ti.title}`); 193 | m.redraw(); // As the delete action is done async, force a redraw when done. 194 | }, 195 | } as ITreeOptions; 196 | 197 | return { 198 | view: () => { 199 | // console.log('Drawing the view...'); 200 | return m('.row', [ 201 | m('.col.s6', [ 202 | m('h3', 'CRUD'), 203 | m(TreeContainer, { tree, options: optionsCRUD, selectedId }), 204 | m('h3', 'Readonly'), 205 | m(TreeContainer, { tree, options }), 206 | m('h3', 'Own view, maxDepth 3'), 207 | m(TreeContainer, { tree, options: optionsOwnView }), 208 | m('h3', 'CRUD, isOpen undefined'), 209 | m(TreeContainer, { tree, options: optionsCRUDisOpen }), 210 | m('h3', 'CRUD, async. create and delete'), 211 | m(TreeContainer, { tree, options: optionsAsync }), 212 | m('h3', 'CRUD, empty tree'), 213 | m(TreeContainer, { tree: emptyTree, options: optionsCRUD }), 214 | ]), 215 | m('.col.s6', [ 216 | m( 217 | 'button.waves-effect.waves-light.btn', 218 | { 219 | onclick: () => { 220 | setTimeout(() => { 221 | data[0].title = `Updated at ${new Date().toTimeString()}`; 222 | m.redraw(); 223 | }, 1000); 224 | }, 225 | }, 226 | 'Update first node with timeout' 227 | ), 228 | m('h3', 'Tree data'), 229 | m('pre', m('code', JSON.stringify(tree, null, 2))), 230 | ]), 231 | ]); 232 | }, 233 | }; 234 | }; 235 | -------------------------------------------------------------------------------- /packages/example/src/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | -webkit-box-sizing: border-box; 5 | -moz-box-sizing: border-box; 6 | box-sizing: border-box; 7 | } 8 | 9 | .main { 10 | padding: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /packages/example/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert an item array to a tree. Assumes each item has a parentId. 3 | * @param items Items 4 | */ 5 | export const unflatten = ( 6 | entities: T[], 7 | // parent = {} as T & ITree, 8 | // tree = [] as Array> 9 | parent = { id: null } as { id: string | number | null; children?: T[] }, 10 | tree = [] as Array 11 | ) => { 12 | const children = (parent.id 13 | ? entities.filter(entity => entity.parentId === parent.id) 14 | : entities.filter(entity => !entity.parentId)) as Array; 15 | 16 | if (children.length > 0) { 17 | if (!parent.id) { 18 | tree = children; 19 | } else { 20 | parent.children = children; 21 | } 22 | children.map(child => unflatten(entities, child)); 23 | } 24 | 25 | return tree; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": 5 | "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 6 | "module": 7 | "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | "lib": [ 9 | "dom", 10 | "es5", 11 | "es2015.promise", 12 | "es2017" 13 | ] /* Specify library files to be included in the compilation. */, 14 | // "allowJs": true, /* Allow javascript files to be compiled. */ 15 | // "checkJs": true, /* Report errors in .js files. */ 16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 17 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 18 | "sourceMap": true /* Generates corresponding '.map' file. */, 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist" /* Redirect output structure to the directory. */, 21 | "rootDir": 22 | "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true /* Enable all strict type-checking options. */, 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | }, 65 | "parcelTsPluginOptions": { 66 | // If true type-checking is disabled 67 | "transpileOnly": false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/example/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "no-console": false, 7 | "no-debugger": false, 8 | "quotemark": [true, "single"], 9 | "trailing-comma": [ 10 | true, 11 | { 12 | "multiline": { 13 | "objects": "always", 14 | "arrays": "always", 15 | "functions": "never", 16 | "typeLiterals": "ignore" 17 | }, 18 | "esSpecCompliant": true 19 | } 20 | ], 21 | "object-literal-sort-keys": false, 22 | "ordered-imports": false, 23 | "arrow-parens": [false, "ban-single-arg-parens"] 24 | }, 25 | "rulesDirectory": [] 26 | } 27 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = false 10 | 11 | # 2 space indentation 12 | [**.*] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .rpt2_cache 3 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .editorconfig 3 | .gitignore 4 | .cache 5 | src 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | tsconfig.json 10 | tslint.json 11 | *.logs 12 | .rpt2_cache 13 | cssnano.config.js 14 | rollup.config.js 15 | img -------------------------------------------------------------------------------- /packages/mithril-tree-component/README.md: -------------------------------------------------------------------------------- 1 | # mithril-tree-component 2 | 3 | A tree component for [Mithril](https://mithril.js.org) that supports drag-and-drop, as well as selecting, creating and deleting items. You can play with it [here](https://erikvullings.github.io/mithril-tree-component/#!/). 4 | 5 | ## Functionality 6 | 7 | - Drag-and-drop to move items (if `editable.canUpdate` is true). 8 | - Create and delete tree items (if `editable.canDelete` and `editable.canDeleteParent` is true). 9 | - Configurable properties for: 10 | - `id` property: unique id of the item. 11 | - `parentId` property: id of the parent. 12 | - `name` property: display title. Alternatively, provide your own component. 13 | - `maxDepth`: when specified, and editable.canCreate is true, do not add children that would exceed this depth, where depth is 0 for root items, 1 for children, etc. 14 | - `create`: function to be used to add your own TreeItem creation logic. 15 | - `selectedId`: can be set in the `TreeContainer` to select a tree item automatically. 16 | - `isOpen`: to indicate whether the tree should show the children. By default, when nothing is set, the `treeItem.isOpen` property is used. If `isOpen` is a string, `treeItem[isOpen]` is used instead. In case `isOpen` is `undefined`, the open/close state is maintained internally. Finally, you can also use a function `(id: string, action: 'get' | 'set', value?: boolean) => boolean | void`, in which case you can maintain the open/close state externally, e.g. for synchronization between different components: 17 | 18 | ```ts 19 | const isOpen = (() => { 20 | const store: Record = {}; 21 | return (id: string, action: 'get' | 'set', value?: boolean) => { 22 | if (action === 'get') { 23 | return store.hasOwnProperty(id) ? store[id] : false; 24 | } else if (typeof value !== 'undefined') { 25 | store[id] = value; 26 | } 27 | }; 28 | })(); 29 | ``` 30 | 31 | - Callback events: 32 | - `onSelect`: when a tree item is selected. 33 | - `onToggle`: when a tree item is expanded or closed. 34 | - `onBefore`[Create | Update | Delete]: can be used to intercept (and block) tree item actions. If the onBeforeX call returns false, the action is stopped. 35 | - `on[Create | Update | Delete]`: when the creation is done. 36 | - When using async functions or promises, please make sure to call `m.redraw()` when you are done. 37 | 38 | This repository contains two projects: 39 | 40 | - An example project, showcasing the usage of the component. 41 | - The mithril-tree-component itself. 42 | 43 | ## Changes 44 | 45 | ### 0.7.1 No breaking changes 46 | 47 | - `selectedId`: can be set in the `TreeContainer` to select a tree item automatically. In this case, you need to handle the `onSelect` event yourself in order to select the item, as in the example. 48 | - When creating a new item, this item is selected automatically. 49 | 50 | ### 0.7.0 No breaking changes 51 | 52 | - Added `onToggle` callback event. 53 | - Added option to maintain the open/close tree state externally by setting the `isOpen` property to a function. 54 | 55 | ### 0.6.4 No breaking changes 56 | 57 | - Fixed: Do not crash when `onCreate` is not defined. 58 | 59 | ### 0.6.1 No breaking changes 60 | 61 | - Fixed `onBeforeUpdate` checks, so the code using this library can determine whether a drop is valid. 62 | 63 | ### 0.6.0 No breaking changes 64 | 65 | - Improved drag-n-drop behaviour: drop above or below an item to create a sibling, drop on an item to create a child. Also the cursor now indicates correctly `no-drop` zones. 66 | - Long lines use `text-overflow: ellipsis`. 67 | - Adding a child is now done by clicking its parent '+' sign (instead of underneath). The advantage is that the tree does not become longer anymore. 68 | - The add '+' and delete 'x' action buttons are swapped, so when adding multiple children, the add symbol keeps the same position. 69 | 70 | ### 0.5.2 71 | 72 | - Publishing two libraries: `mithril-tree-component.js` and `mithril-tree-component.mjs`, the latter using ES modules. 73 | - Integrated `css` inside the component, so you do not need to load a separate stylesheet. 74 | - Using BEM naming convention for the `css`, and prefix every class with `mtc-` to make their names more unique. 75 | - Improved usability: 76 | - display a placeholder for the empty tree. 77 | - only show the action buttons when hovering, and only then, create vertical space for them (so the tree is more continuous). 78 | 79 | ### 0.4.0 80 | 81 | This version of the tree component: 82 | 83 | - Does no longer require a tree-layout: input is a flat Array of items, and the `children` are resolved by processing all items based on the `parentId` property. So the `children` property is no longer used. 84 | - Does not mutate the tree components anymore if you keep the `isOpen` property undefined. 85 | 86 | ### 0.3.6 87 | 88 | - maxDepth: can be set to limit the ability to create children. 89 | - treeItemView: to provide your own view component 90 | - open or close state when displaying children: can be maintained internally, so different components do not share open/close state. 91 | 92 | ## Usage 93 | 94 | ```bash 95 | npm i mithril-tree-component 96 | ``` 97 | 98 | From the [example project](../example). There you can also find some CSS styles. 99 | 100 | ```ts 101 | import m from 'mithril'; 102 | import { unflatten } from '../utils'; 103 | import { TreeContainer, ITreeOptions, ITreeItem, uuid4 } from 'mithril-tree-component'; 104 | 105 | interface IMyTree extends ITreeItem { 106 | id: number | string; 107 | parentId: number | string; 108 | title: string; 109 | } 110 | 111 | export const TreeView = () => { 112 | const data: IMyTree[] = [ 113 | { id: 1, parentId: 0, title: 'My id is 1' }, 114 | { id: 2, parentId: 1, title: 'My id is 2' }, 115 | { id: 3, parentId: 1, title: 'My id is 3' }, 116 | { id: 4, parentId: 2, title: 'My id is 4' }, 117 | { id: 5, parentId: 0, title: 'My id is 5' }, 118 | { id: 6, parentId: 0, title: 'My id is 6' }, 119 | { id: 7, parentId: 4, title: 'My id is 7' }, 120 | ]; 121 | const tree = unflatten(data); 122 | const options = { 123 | id: 'id', 124 | parentId: 'parentId', 125 | isOpen: 'isOpen', 126 | name: 'title', 127 | onSelect: (ti, isSelected) => console.log(`On ${isSelected ? 'select' : 'unselect'}: ${ti.title}`), 128 | onBeforeCreate: ti => console.log(`On before create ${ti.title}`), 129 | onCreate: ti => console.log(`On create ${ti.title}`), 130 | onBeforeDelete: ti => console.log(`On before delete ${ti.title}`), 131 | onDelete: ti => console.log(`On delete ${ti.title}`), 132 | onBeforeUpdate: (ti, action, newParent) => 133 | console.log(`On before ${action} update ${ti.title} to ${newParent ? newParent.title : ''}.`), 134 | onUpdate: ti => console.log(`On update ${ti.title}`), 135 | create: (parent?: IMyTree) => { 136 | const item = {} as IMyTree; 137 | item.id = uuid4(); 138 | if (parent) { 139 | item.parentId = parent.id; 140 | } 141 | item.title = `Created at ${new Date().toLocaleTimeString()}`; 142 | return item as ITreeItem; 143 | }, 144 | editable: { canCreate: true, canDelete: true, canUpdate: true, canDeleteParent: false }, 145 | } as ITreeOptions; 146 | return { 147 | view: () => 148 | m('.row', [ 149 | m('.col.s6', [m('h3', 'Mithril-tree-component'), m(TreeContainer, { tree, options })]), 150 | m('.col.s6', [m('h3', 'Tree data'), m('pre', m('code', JSON.stringify(tree, null, 2)))]), 151 | ]), 152 | }; 153 | }; 154 | ``` 155 | 156 | ## Build instructions 157 | 158 | This repository uses [Lerna](https://lernajs.io) to manage multiple projects (packages) in one repository. To compile and run both packages, proceed as follows (from the root): 159 | 160 | ```bash 161 | npm i 162 | npm start 163 | ``` 164 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/cssnano.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: [ 3 | 'default', 4 | { 5 | calc: false, 6 | discardComments: { 7 | removeAll: true 8 | } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/mithril-tree-component/img/mithril-tree-component-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikvullings/mithril-tree-component/a9cb68ab2906d68e73ec172dab45c559346aa65c/packages/mithril-tree-component/img/mithril-tree-component-animation.gif -------------------------------------------------------------------------------- /packages/mithril-tree-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-tree-component", 3 | "version": "0.7.1", 4 | "description": "A tree component for the Mitrhil framework.", 5 | "main": "dist/mithril-tree-component.js", 6 | "module": "dist/mithril-tree-component.mjs", 7 | "browser": "dist/mithril-tree-component.mjs", 8 | "typings": "dist/index.d.ts", 9 | "scripts": { 10 | "link": "pnpm link", 11 | "start": "rollup -c -w", 12 | "build": "npm run build:production", 13 | "clean": "rimraf ./dist ./node_modules/.ignored", 14 | "build:production": "npm run clean && rollup -c", 15 | "build:domain": "typedoc --out ../../docs/typedoc src", 16 | "patch-release": "npm run build && npm version patch --force -m \"Patch release\" && npm publish && git push --follow-tags", 17 | "minor-release": "npm run build && npm version minor --force -m \"Minor release\" && npm publish && git push --follow-tags", 18 | "major-release": "npm run build && npm version major --force -m \"Major release\" && npm publish && git push --follow-tags" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/erikvullings/mithril-tree-component.git" 23 | }, 24 | "keywords": [ 25 | "mithril", 26 | "tree", 27 | "component", 28 | "typescript" 29 | ], 30 | "author": "Erik Vullings (http://www.tno.nl)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/erikvullings/mithril-tree-component/issues" 34 | }, 35 | "homepage": "https://github.com/erikvullings/mithril-tree-component#readme", 36 | "devDependencies": { 37 | "@types/mithril": "github:MithrilJS/mithril.d.ts#v2", 38 | "autoprefixer": "^10.0.1", 39 | "rollup": "^2.32.1", 40 | "rollup-plugin-commonjs": "^10.1.0", 41 | "rollup-plugin-json": "^4.0.0", 42 | "rollup-plugin-node-resolve": "^5.2.0", 43 | "rollup-plugin-postcss": "^3.1.8", 44 | "rollup-plugin-sourcemaps": "^0.6.3", 45 | "rollup-plugin-terser": "^7.0.2", 46 | "rollup-plugin-typescript2": "^0.28.0", 47 | "cssnano": "^4.1.10", 48 | "postcss-cssnext": "^3.1.0", 49 | "tslib": "^2.0.3", 50 | "typedoc": "^0.19.2", 51 | "typescript": "^4.0.5", 52 | "rimraf": "^3.0.2", 53 | "set-value": ">=3.0.2", 54 | "mixin-deep": ">=2.0.1", 55 | "lodash.template": ">=4.5.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import sourceMaps from 'rollup-plugin-sourcemaps'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import postcss from 'rollup-plugin-postcss'; 6 | import json from 'rollup-plugin-json'; 7 | import cssnext from 'postcss-cssnext'; 8 | import cssnano from 'cssnano'; 9 | import { terser } from 'rollup-plugin-terser'; 10 | 11 | const pkg = require('./package.json'); 12 | const production = !process.env.ROLLUP_WATCH; 13 | 14 | export default { 15 | input: `src/index.ts`, 16 | watch: 'src/**', 17 | context: 'null', 18 | moduleContext: 'null', 19 | output: [ 20 | { 21 | file: pkg.module, 22 | format: 'es', 23 | sourcemap: true, 24 | }, 25 | { 26 | file: pkg.main, 27 | format: 'iife', 28 | name: 'TreeContainer', 29 | sourcemap: true, 30 | globals: { 31 | mithril: 'm', 32 | }, 33 | }, 34 | ], 35 | // Indicate here external modules you don't want to include in your bundle 36 | external: ['mithril'], 37 | // external: [...Object.keys(pkg.dependencies || {})], 38 | watch: { 39 | include: 'src/**', 40 | }, 41 | plugins: [ 42 | // Allow json resolution 43 | json(), 44 | postcss({ 45 | extensions: ['.css'], 46 | plugins: [cssnext({ warnForDuplicates: false }), cssnano()], 47 | }), 48 | // Compile TypeScript files 49 | typescript({ 50 | rollupCommonJSResolveHack: true, 51 | typescript: require('typescript'), 52 | }), 53 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 54 | commonjs(), 55 | // Allow node_modules resolution, so you can use 'external' to control 56 | // which external modules to include in the bundle 57 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 58 | resolve({ 59 | customResolveOptions: { 60 | moduleDirectory: 'node_modules', 61 | }, 62 | }), 63 | // Resolve source maps to the original source 64 | sourceMaps(), 65 | // minifies generated bundles 66 | production && terser(), 67 | ], 68 | }; 69 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/declarations/css.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/declarations/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | export * from './utils'; 3 | export * from './tree-container'; 4 | export * from './tree-item'; 5 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tree-item'; 2 | export { ITreeOptions, TreeItemAction, TreeItemUpdateAction, ITreeItemViewComponent } from './tree-options'; 3 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/models/tree-item.ts: -------------------------------------------------------------------------------- 1 | export interface ITreeItem { 2 | [key: string]: any; 3 | } 4 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/models/tree-options.ts: -------------------------------------------------------------------------------- 1 | import { Component, Attributes } from 'mithril'; 2 | import { ITreeItem } from '.'; 3 | 4 | /** Indicates the type of action is performed on the tree item. */ 5 | export type TreeItemAction = 'create' | 'delete' | 'add_child' | 'expand_more' | 'expand_less'; 6 | 7 | /** Indicates the type of UPDATE action is performed on the tree item. */ 8 | export type TreeItemUpdateAction = 'edit' | 'move'; 9 | 10 | export interface ITreeItemViewComponent { 11 | width: number; 12 | treeItem: ITreeItem; 13 | depth: number; 14 | } 15 | 16 | export interface ITreeOptions { 17 | tree: ITreeItem[]; 18 | /** If provided, this component is used to display the tree item. */ 19 | treeItemView: Component; 20 | /** Name of the name property, e.g. how the tree item is displayed in the tree (default 'name') */ 21 | name: string; 22 | /** Name of the ID property (default 'id') */ 23 | id: string; 24 | /** Name of the parent ID property (default 'parentId') */ 25 | parentId: string; 26 | /** Name of the open property, e.g. to display or hide the children (default 'isOpen') */ 27 | isOpen: string | undefined | ((id: string, action: 'get' | 'set', value?: boolean) => void | boolean); 28 | /** 29 | * At what level do you prevent creating new children: 1 is only children, 2 is grandchildren, etc. 30 | * Default is Number.MAX_SAFE_INTEGER. NOTE: It does not prevent you to move items with children. 31 | */ 32 | maxDepth: number; 33 | /** If true (default), you can have multiple root nodes */ 34 | multipleRoots: boolean; 35 | /** If enabled, turn on logging */ 36 | logging: boolean; 37 | /** When a tree item is selected, this function is invoked */ 38 | onSelect: (treeItem: ITreeItem, isSelected: boolean) => void | Promise; 39 | /** When a tree item is opened (expanded) or closed */ 40 | onToggle: (treeItem: ITreeItem, isExpanded: boolean) => void | Promise; 41 | /** Before a tree item is created, this function is invoked. When it returns false, the action is cancelled. */ 42 | onBeforeCreate: (treeItem: ITreeItem) => boolean | void | Promise; 43 | /** When a tree item has been created, this function is invoked */ 44 | onCreate: (treeItem: ITreeItem) => void | Promise; 45 | /** Before a tree item is deleted, this function is invoked. When it returns false, the action is cancelled. */ 46 | onBeforeDelete: (treeItem: ITreeItem) => boolean | void | Promise; 47 | /** When a tree item has been deleted, this function is invoked */ 48 | onDelete: (treeItem: ITreeItem) => void | Promise; 49 | /** Before a tree item has been updated, this function is invoked. When it returns false, the action is cancelled. */ 50 | onBeforeUpdate: ( 51 | treeItem: ITreeItem, 52 | action?: TreeItemUpdateAction, 53 | newParent?: ITreeItem 54 | ) => boolean | void | Promise; 55 | /** When a tree item has been updated, this function is invoked */ 56 | onUpdate: (treeItem: ITreeItem, action?: TreeItemUpdateAction, newParent?: ITreeItem) => void | Promise; 57 | /** 58 | * Factory function that can be used to create new items. If there is no parent, the depth is -1. 59 | * If parent treeItem is missing, a root item should be created. 60 | */ 61 | create: (parent?: ITreeItem, depth?: number, width?: number) => ITreeItem | Promise; 62 | /** Does the tree support editing, e.g. creating, deleting or updating. */ 63 | editable: Partial<{ 64 | /** Allow creating of new items. */ 65 | canCreate: boolean; 66 | /** Allow deleting of items. */ 67 | canDelete: boolean; 68 | /** Allow deleting of items that are parents (so all children would be deleted too). */ 69 | canDeleteParent: boolean; 70 | /** Allow updating of items. */ 71 | canUpdate: boolean; 72 | }>; 73 | /** 74 | * Component to display icons to create, delete, etc. 75 | * The component will receive an onclick attribute to perform its function. 76 | */ 77 | button: (name: TreeItemAction) => Component; 78 | /** When the tree is empty, what text do you want to show. Default 'Create your first item' */ 79 | placeholder: string; 80 | } 81 | 82 | export interface IInternalTreeOptions extends ITreeOptions { 83 | /** Internal function: retrieves the tree item based on its id */ 84 | _find: (id: string | number) => ITreeItem | undefined; 85 | _findChildren: (treeItem: ITreeItem) => ITreeItem[]; 86 | /** Internal function: creates a sibling tree item */ 87 | _createItem: (siblingId?: string | number, width?: number) => void; 88 | _deleteItem: (id?: string | number) => void; 89 | _hasChildren: (treeItem: ITreeItem) => boolean; 90 | _addChildren: (treeItem: ITreeItem, width?: number) => void; 91 | _depth: (treeItem: ITreeItem, curDepth?: number) => number; 92 | _isExpanded: (treeItem: ITreeItem) => boolean; 93 | } 94 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/models/tree-state.ts: -------------------------------------------------------------------------------- 1 | import { IInternalTreeOptions } from './tree-options'; 2 | import { Attributes } from 'mithril'; 3 | import { ITreeItem } from '.'; 4 | 5 | export interface ITreeState { 6 | tree?: ITreeItem[]; 7 | /** Name of the parent ID property (default 'parentId') */ 8 | parentId: string; 9 | /** ID of the selected tree item */ 10 | selectedId?: string | number; 11 | /** ID of the tree item that is being dragged */ 12 | dragId?: string | number; 13 | /** Options for the tree */ 14 | options: IInternalTreeOptions; 15 | /** Options for dragging */ 16 | dragOptions: Attributes; 17 | /** Width of the item */ 18 | width: number; 19 | /** When dragging, set this to true */ 20 | isDragging: boolean; 21 | } 22 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/styles/tree-container.css: -------------------------------------------------------------------------------- 1 | .mtc .mtc__clearfix::after { 2 | content: ''; 3 | clear: both; 4 | display: table; 5 | } 6 | 7 | .mtc { 8 | padding-left: 0; 9 | margin: 0.5rem 0 1rem 0; 10 | border: 1px solid #e0e0e0; 11 | border-radius: 2px; 12 | overflow: hidden; 13 | position: relative; 14 | white-space: nowrap; 15 | } 16 | .mtc.mtc__empty { 17 | padding: 1em 0; 18 | } 19 | .mtc .mtc__clickable { 20 | cursor: pointer; 21 | margin: 0 2px; 22 | } 23 | 24 | .mtc .mtc__collapse-expand-item { 25 | margin: 0 0.6rem; 26 | } 27 | 28 | .mtc .mtc__childless-item { 29 | margin-left: 2rem; 30 | } 31 | 32 | .mtc .mtc__indent { 33 | margin: 0 1.5rem; 34 | } 35 | 36 | .mtc .mtc__moveable { 37 | cursor: move; 38 | } 39 | 40 | .mtc .mtc__item { 41 | cursor: pointer; 42 | } 43 | 44 | .mtc .mtc__header { 45 | line-height: 2rem; 46 | width: 100%; 47 | } 48 | 49 | .mtc .mtc__act-group, 50 | .mtc .mtc__header div { 51 | display: inline-block; 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | vertical-align: middle; 55 | } 56 | 57 | .mtc .mtc__header.active { 58 | background: #e0e0e0; 59 | } 60 | 61 | .mtc .mtc__header:focus { 62 | color: blue; 63 | background-color: #cf5c3f; 64 | } 65 | 66 | .mtc .mtc__header a { 67 | float: left; 68 | color: #f2f2f2; 69 | text-align: center; 70 | padding: 14px 16px; 71 | text-decoration: none; 72 | font-size: 17px; 73 | } 74 | 75 | /* .mtc .mtc__branch .mtc__act, */ 76 | .mtc .mtc__item .mtc__act-group { 77 | visibility: hidden; 78 | opacity: 0; 79 | transition: visibility 0s, opacity 0.5s linear; 80 | float: right; 81 | display: none; 82 | } 83 | .mtc .mtc__item .mtc__act-group { 84 | float: right; 85 | } 86 | .mtc .mtc__header:hover .mtc__act-group { 87 | visibility: visible; 88 | opacity: 1; 89 | display: inline-block; 90 | } 91 | 92 | .mtc .mtc__act { 93 | font-weight: bold; 94 | cursor: pointer; 95 | display: inline-block; 96 | padding: 0 0.3rem; 97 | } 98 | .mtc .mtc__act i { 99 | font-weight: normal; 100 | cursor: pointer; 101 | display: inline-block; 102 | padding: 0 0.3rem; 103 | } 104 | .mtc .mtc__act:hover { 105 | background: #e0e0e0; 106 | } 107 | /* 108 | .mtc .mtc__branch .mtc__act { 109 | visibility: hidden; 110 | opacity: 0; 111 | transition: visibility 0s, opacity 0.5s linear; 112 | display: none; 113 | } */ 114 | 115 | .mtc .mtc__branch:hover .mtc__act { 116 | visibility: visible; 117 | opacity: 1; 118 | display: inline-block; 119 | } 120 | 121 | .mtc ul.mtc__item-body { 122 | padding-left: 0.8rem; 123 | } 124 | 125 | .mtc ul.mtc__item-body > li { 126 | white-space: nowrap; 127 | } 128 | 129 | .mtc span.mtc__item-title { 130 | margin-right: 1rem; 131 | } 132 | 133 | .mtc .mtc__draggable { 134 | cursor: move; /* fallback if grab cursor is unsupported */ 135 | cursor: grab; 136 | cursor: -moz-grab; 137 | cursor: -webkit-grab; 138 | } 139 | 140 | /* (Optional) Apply a "closed-hand" cursor during drag operation. */ 141 | .mtc .mtc__draggable:active { 142 | cursor: grabbing; 143 | cursor: -moz-grabbing; 144 | cursor: -webkit-grabbing; 145 | } 146 | 147 | .mtc .mtc__as_child { 148 | /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#1e5799+0,2989d8+50,207cca+51,7db9e8+100&0+0,0.5+51,0+100 */ 149 | background: -moz-linear-gradient( 150 | top, 151 | rgba(30, 87, 153, 0) 0%, 152 | rgba(41, 137, 216, 0.49) 50%, 153 | rgba(32, 124, 202, 0.5) 51%, 154 | rgba(125, 185, 232, 0) 100% 155 | ); /* FF3.6-15 */ 156 | background: -webkit-linear-gradient( 157 | top, 158 | rgba(30, 87, 153, 0) 0%, 159 | rgba(41, 137, 216, 0.49) 50%, 160 | rgba(32, 124, 202, 0.5) 51%, 161 | rgba(125, 185, 232, 0) 100% 162 | ); /* Chrome10-25,Safari5.1-6 */ 163 | background: linear-gradient( 164 | to bottom, 165 | rgba(30, 87, 153, 0) 0%, 166 | rgba(41, 137, 216, 0.49) 50%, 167 | rgba(32, 124, 202, 0.5) 51%, 168 | rgba(125, 185, 232, 0) 100% 169 | ); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 170 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#001e5799', endColorstr='#007db9e8',GradientType=0 ); /* IE6-9 */ 171 | } 172 | 173 | .mtc .mtc__below { 174 | /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#1e5799+100,1e5799+100,1e5799+100&0+0,0.7+100 */ 175 | background: -moz-linear-gradient(top, rgba(30, 87, 153, 0) 0%, rgba(30, 87, 153, 0.7) 100%); /* FF3.6-15 */ 176 | background: -webkit-linear-gradient( 177 | top, 178 | rgba(30, 87, 153, 0) 0%, 179 | rgba(30, 87, 153, 0.7) 100% 180 | ); /* Chrome10-25,Safari5.1-6 */ 181 | background: linear-gradient( 182 | to bottom, 183 | rgba(30, 87, 153, 0) 0%, 184 | rgba(30, 87, 153, 0.7) 100% 185 | ); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 186 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#001e5799', endColorstr='#b31e5799',GradientType=0 ); /* IE6-9 */ 187 | } 188 | 189 | .mtc .mtc__above { 190 | /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#1e5799+0,1e5799+0,1e5799+0&0.7+0,0+100 */ 191 | background: -moz-linear-gradient(top, rgba(30, 87, 153, 0.7) 0%, rgba(30, 87, 153, 0) 100%); /* FF3.6-15 */ 192 | background: -webkit-linear-gradient( 193 | top, 194 | rgba(30, 87, 153, 0.7) 0%, 195 | rgba(30, 87, 153, 0) 100% 196 | ); /* Chrome10-25,Safari5.1-6 */ 197 | background: linear-gradient( 198 | to bottom, 199 | rgba(30, 87, 153, 0.7) 0%, 200 | rgba(30, 87, 153, 0) 100% 201 | ); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 202 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#b31e5799', endColorstr='#001e5799',GradientType=0 ); /* IE6-9 */ 203 | } 204 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/tree-container.ts: -------------------------------------------------------------------------------- 1 | import './styles/tree-container.css'; 2 | import m, { FactoryComponent, Attributes, VnodeDOM } from 'mithril'; 3 | import { TreeItem, TreeItemIdPrefix } from './tree-item'; 4 | import { IInternalTreeOptions } from './models/tree-options'; 5 | import { ITreeItem, ITreeOptions, TreeItemUpdateAction } from './models'; 6 | import { uuid4, TreeButton, move } from './utils'; 7 | import { ITreeState } from './models/tree-state'; 8 | 9 | export let log: (...args: any[]) => void = () => undefined; 10 | 11 | export const TreeContainer: FactoryComponent<{ 12 | tree: ITreeItem[]; 13 | options: Partial; 14 | /** Optional id of the tree item that is selected */ 15 | selectedId?: string | number; 16 | }> = () => { 17 | const state = { 18 | selectedId: '', 19 | dragId: '', 20 | } as ITreeState; 21 | 22 | const setDefaultOptions = (options: Partial) => { 23 | // const { options } = state; 24 | const wrapper = ( 25 | defaultFn: (treeItem: ITreeItem, action?: TreeItemUpdateAction, newParent?: ITreeItem) => void, 26 | beforeFn?: ( 27 | treeItem: ITreeItem, 28 | action?: TreeItemUpdateAction, 29 | newParent?: ITreeItem 30 | ) => boolean | void | Promise, 31 | afterFn?: (treeItem: ITreeItem, action?: TreeItemUpdateAction, newParent?: ITreeItem) => void | Promise 32 | ) => async (treeItem: ITreeItem, action?: TreeItemUpdateAction, newParent?: ITreeItem) => { 33 | if (beforeFn) { 34 | const before = beforeFn(treeItem, action, newParent); 35 | const isAsync = typeof before !== 'boolean'; 36 | const result = isAsync ? await before : before; 37 | if (result === false) { 38 | return; 39 | } 40 | } 41 | await Promise.resolve(defaultFn(treeItem, action, newParent)); 42 | if (afterFn) { 43 | await Promise.resolve(afterFn(treeItem, action, newParent)); 44 | } 45 | }; 46 | const opts = { 47 | id: 'id', 48 | parentId: 'parentId', 49 | name: 'name', 50 | isOpen: 'isOpen', 51 | maxDepth: Number.MAX_SAFE_INTEGER, 52 | multipleRoots: true, 53 | logging: false, 54 | editable: { 55 | canCreate: false, 56 | canDelete: false, 57 | canUpdate: false, 58 | canDeleteParent: false, 59 | }, 60 | placeholder: 'Create your first item', 61 | ...options, 62 | } as IInternalTreeOptions; 63 | 64 | if (opts.logging) { 65 | log = console.log; 66 | } 67 | if (!opts.isOpen) { 68 | opts.isOpen = (() => { 69 | const store: Record = {}; 70 | return (id: string, action: 'get' | 'set', value?: boolean): boolean | void => { 71 | if (action === 'get') { 72 | return store.hasOwnProperty(id) ? store[id] : false; 73 | } else if (typeof value !== 'undefined') { 74 | store[id] = value; 75 | } 76 | }; 77 | })(); 78 | } 79 | const { id, parentId, name, isOpen } = opts; 80 | 81 | /** Recursively find a tree item */ 82 | const find = (tId: string | number = '', partialTree = state.tree) => { 83 | if (!tId || !partialTree) { 84 | return undefined; 85 | } 86 | let found: ITreeItem | undefined; 87 | partialTree.some((treeItem) => { 88 | if (treeItem[id] === tId) { 89 | found = treeItem; 90 | return true; 91 | } 92 | return false; 93 | }); 94 | return found; 95 | }; 96 | 97 | /** Recursively delete a tree item and all its children */ 98 | const deleteTreeItem = (tId: string | number = '', partialTree = state.tree) => { 99 | if (!tId || !partialTree) { 100 | return false; 101 | } 102 | let found = false; 103 | partialTree.some((treeItem, i) => { 104 | if (treeItem[id] === tId) { 105 | partialTree.splice(i, 1); 106 | findChildren(treeItem).forEach((ti) => deleteTreeItem(ti[id])); 107 | found = true; 108 | return true; 109 | } 110 | return false; 111 | }); 112 | return found; 113 | }; 114 | 115 | /** Recursively update a tree item and all its children */ 116 | const updateTreeItem = (updatedTreeItem: ITreeItem, partialTree = state.tree) => { 117 | if (!partialTree) { 118 | return false; 119 | } 120 | let found = false; 121 | partialTree.some((treeItem, i) => { 122 | if (treeItem[id] === updatedTreeItem[id]) { 123 | partialTree[i] = updatedTreeItem; 124 | found = true; 125 | return true; 126 | } 127 | return false; 128 | }); 129 | return found; 130 | }; 131 | 132 | const depth = (treeItem: ITreeItem, curDepth = 0): number => { 133 | const pId = treeItem[parentId]; 134 | return pId ? depth(find(pId) as ITreeItem, curDepth + 1) : curDepth; 135 | }; 136 | 137 | const onCreate = wrapper( 138 | (ti: ITreeItem) => { 139 | if (state.tree) { 140 | state.tree.push(ti); 141 | } 142 | }, 143 | opts.onBeforeCreate, 144 | (treeItem: ITreeItem) => { 145 | if (opts.onCreate) { 146 | opts.onCreate(treeItem); 147 | } 148 | onSelect(treeItem, true); 149 | } 150 | ); 151 | 152 | /** Create a new tree item. */ 153 | const createTreeItem = (pId: string | number = '', width: number) => { 154 | const create = (w: number) => { 155 | if (options.create) { 156 | const parent = find(pId); 157 | const d = parent ? depth(parent) : -1; 158 | return options.create(parent, d, w); 159 | } 160 | const item = {} as ITreeItem; 161 | item[id] = uuid4(); 162 | item[parentId] = pId; 163 | item[name] = 'New...'; 164 | return item; 165 | }; 166 | onCreate(create(width)); 167 | }; 168 | 169 | const onDelete = wrapper((ti: ITreeItem) => deleteTreeItem(ti[id]), opts.onBeforeDelete, opts.onDelete); 170 | 171 | const onUpdate = wrapper( 172 | (ti: ITreeItem, _: TreeItemUpdateAction = 'edit', __?: ITreeItem) => updateTreeItem(ti), 173 | opts.onBeforeUpdate, 174 | opts.onUpdate 175 | ); 176 | 177 | const onSelect = (ti: ITreeItem, isSelected: boolean) => { 178 | state.selectedId = isSelected ? ti[id] : ''; 179 | if (opts.onSelect) { 180 | opts.onSelect(ti, isSelected); 181 | } 182 | }; 183 | 184 | const onToggle = (ti: ITreeItem, isExpanded: boolean) => { 185 | if (opts.onToggle) { 186 | opts.onToggle(ti, isExpanded); 187 | } 188 | }; 189 | 190 | const treeItemView = opts.treeItemView || { 191 | view: ({ attrs: { treeItem } }) => treeItem[name], 192 | }; 193 | 194 | /** The drop location indicates the new position of the dropped element: above, below or as a child */ 195 | const computeDropLocation = (target: HTMLElement, ev: DragEvent) => { 196 | const { top, height } = target.getBoundingClientRect(); 197 | const y = ev.clientY - top; 198 | const deltaZone = height / 3; 199 | return y < deltaZone ? 'above' : y < 2 * deltaZone ? 'as_child' : 'below'; 200 | }; 201 | 202 | const convertId = (cid: string | number) => (isNaN(+cid) ? cid : +cid); 203 | 204 | const dndTreeItems = (target: HTMLElement, ev: DragEvent) => { 205 | if (ev.dataTransfer) { 206 | const sourceId = convertId(state.dragId || ev.dataTransfer.getData('text').replace(TreeItemIdPrefix, '')); 207 | const targetId = convertId((findId(target) || '').replace(TreeItemIdPrefix, '')); 208 | const tiSource = find(sourceId); 209 | const tiTarget = find(targetId); 210 | return { tiSource, tiTarget, sourceId, targetId }; 211 | } 212 | return { 213 | tiSource: undefined, 214 | tiTarget: undefined, 215 | sourceId: undefined, 216 | targetId: undefined, 217 | }; 218 | }; 219 | 220 | const isValidTarget = ( 221 | target: HTMLElement, 222 | ev: DragEvent, 223 | dropLocation: 'above' | 'below' | 'as_child' = computeDropLocation(target, ev) 224 | ) => { 225 | const { sourceId, targetId, tiSource, tiTarget } = dndTreeItems(target, ev); 226 | const parent = dropLocation === 'as_child' || !tiTarget ? tiTarget : find(tiTarget[parentId]); 227 | return ( 228 | targetId !== sourceId && 229 | (!opts.onBeforeUpdate || (tiSource && opts.onBeforeUpdate(tiSource, 'move', parent) === true)) 230 | ); 231 | }; 232 | 233 | const dragOpts = { 234 | ondrop: (ev: DragEvent) => { 235 | if (!ev.dataTransfer || !ev.target) { 236 | return false; 237 | } 238 | const target = ev.target as HTMLElement; 239 | const parent = findParent(target); 240 | if (!parent) { 241 | return; 242 | } 243 | parent.classList.remove('mtc__above', 'mtc__below', 'mtc__as_child'); 244 | state.isDragging = false; 245 | ev.preventDefault(); // do not open a link 246 | const { sourceId, targetId, tiSource, tiTarget } = dndTreeItems(target, ev); 247 | const dropLocation = computeDropLocation(parent, ev); 248 | if (!isValidTarget(target, ev, dropLocation)) { 249 | return false; 250 | } 251 | log(`Dropping ${sourceId} ${dropLocation} ${targetId}`); 252 | if (tiSource && tiTarget) { 253 | tiSource[parentId] = tiTarget[dropLocation === 'as_child' ? id : parentId]; 254 | if (state.tree) { 255 | const sourceIndex = state.tree && state.tree.indexOf(tiSource); 256 | const targetIndex = state.tree && state.tree.indexOf(tiTarget); 257 | const newIndex = Math.max( 258 | 0, 259 | dropLocation === 'above' 260 | ? targetIndex - 1 261 | : dropLocation === 'below' 262 | ? targetIndex + 1 263 | : state.tree.length - 1 264 | ); 265 | move(state.tree, sourceIndex, newIndex); 266 | } 267 | if (dropLocation === 'as_child' && isOpen) { 268 | if (typeof isOpen === 'function') { 269 | isOpen(tiTarget[id], 'set', true); 270 | } else { 271 | tiTarget[isOpen] = true; 272 | } 273 | } 274 | if (opts.onUpdate) { 275 | opts.onUpdate(tiSource, 'move', tiTarget); 276 | } 277 | return true; 278 | } else { 279 | return false; 280 | } 281 | }, 282 | ondragover: (ev: DragEvent) => { 283 | (ev as any).redraw = false; 284 | const target = ev.target as HTMLElement; 285 | const parent = findParent(target); 286 | if (parent) { 287 | parent.classList.remove('mtc__above', 'mtc__below', 'mtc__as_child'); 288 | const dropLocation = computeDropLocation(parent, ev); 289 | if (isValidTarget(target, ev, dropLocation)) { 290 | ev.preventDefault(); 291 | parent.classList.add('mtc__' + dropLocation); 292 | target.style.cursor = isValidTarget(target, ev, dropLocation) ? 'inherit' : 'no_drop'; 293 | } 294 | } else { 295 | target.style.cursor = 'no_drop'; 296 | } 297 | }, 298 | // ondragenter: (ev: DragEvent) => { 299 | // const target = ev.target as HTMLElement; 300 | // const { sourceId, targetId, tiSource, tiTarget } = dndTreeItems(target, ev); 301 | // const disallowDrop = 302 | // targetId === sourceId || 303 | // (tiSource && opts.onBeforeUpdate && opts.onBeforeUpdate(tiSource, 'move', tiTarget) === false); 304 | // target.style.cursor = disallowDrop ? 'no-drop' : ''; 305 | // }, 306 | ondragleave: (ev: DragEvent) => { 307 | const target = ev.target as HTMLElement; 308 | if (target && target.style) { 309 | target.style.cursor = 'inherit'; 310 | } 311 | const parent = findParent(target); 312 | if (parent) { 313 | parent.classList.remove('mtc__above', 'mtc__below', 'mtc__as_child'); 314 | } 315 | }, 316 | ondragstart: (ev: DragEvent) => { 317 | const target = ev.target; 318 | (ev as any).redraw = false; 319 | state.dragId = ''; 320 | if (target && ev.dataTransfer) { 321 | state.isDragging = true; 322 | ev.dataTransfer.setData('text', (target as any).id); 323 | ev.dataTransfer.effectAllowed = 'move'; 324 | state.dragId = (target as any).id.replace(TreeItemIdPrefix, ''); 325 | log('Drag start: ' + ev.dataTransfer.getData('text')); 326 | } 327 | }, 328 | } as Attributes; 329 | 330 | const hasChildren = (treeItem: ITreeItem) => state.tree && state.tree.some((ti) => ti[parentId] === treeItem[id]); 331 | 332 | const addChildren = (treeItem: ITreeItem, width: number) => { 333 | createTreeItem(treeItem[id], width); 334 | }; 335 | 336 | const isExpanded = (treeItem: ITreeItem) => 337 | hasChildren(treeItem) && (typeof isOpen === 'function' ? isOpen(treeItem[id], 'get') : treeItem[isOpen]); 338 | 339 | const findChildren = (treeItem: ITreeItem) => 340 | state.tree ? state.tree.filter((ti) => ti[parentId] === treeItem[id]) : []; 341 | 342 | return { 343 | dragOptions: dragOpts, 344 | options: { 345 | ...opts, 346 | treeItemView, 347 | onSelect, 348 | onToggle, 349 | onCreate, 350 | onDelete, 351 | onUpdate, 352 | _findChildren: findChildren, 353 | _find: find, 354 | _deleteItem: deleteTreeItem, 355 | _createItem: createTreeItem, 356 | _hasChildren: hasChildren, 357 | _addChildren: addChildren, 358 | _depth: depth, 359 | _isExpanded: isExpanded, 360 | } as IInternalTreeOptions, 361 | }; 362 | }; 363 | 364 | /** Find the ID of the first parent element. */ 365 | const findId = (el: HTMLElement | null): string | null => 366 | el ? (el.id ? el.id : el.parentElement ? findId(el.parentElement) : null) : null; 367 | 368 | /** Find the ID of the first parent element. */ 369 | const findParent = (el: HTMLElement | null): HTMLElement | null => 370 | el ? (el.id ? el : el.parentElement ? findParent(el.parentElement) : null) : null; 371 | 372 | const setTopWidth: 373 | | ((this: {}, vnode: VnodeDOM<{ tree: ITreeItem[]; options: Partial }, {}>) => any) 374 | | undefined = ({ dom }) => { 375 | state.width = dom.clientWidth - 64; 376 | }; 377 | 378 | return { 379 | oninit: ({ attrs: { options: treeOptions } }) => { 380 | const { options, dragOptions } = setDefaultOptions(treeOptions); 381 | state.options = options; 382 | state.dragOptions = dragOptions; 383 | state.parentId = options.parentId; 384 | }, 385 | onupdate: setTopWidth, 386 | oncreate: setTopWidth, 387 | view: ({ attrs: { tree, selectedId } }) => { 388 | state.tree = tree; 389 | const { options, dragOptions, parentId, width } = state; 390 | if (!state.tree || !options || !dragOptions) { 391 | return undefined; 392 | } 393 | const { 394 | _createItem, 395 | placeholder, 396 | editable: { canUpdate, canCreate }, 397 | id, 398 | multipleRoots, 399 | } = options; 400 | const isEmpty = state.tree.length === 0; 401 | return m( 402 | '.mtc.mtc__container', 403 | isEmpty 404 | ? m( 405 | '.mtc__empty', 406 | m( 407 | '.mtc__act.mtc__header', 408 | { 409 | onclick: () => _createItem(), 410 | }, 411 | [m('div', '✚'), m('i', placeholder)] 412 | ) 413 | ) 414 | : m( 415 | `[draggable=${canUpdate}]`, 416 | { ...dragOptions }, 417 | m('ul.mtc__branch', [ 418 | ...state.tree 419 | .filter((item) => !item[parentId]) 420 | .map((item) => 421 | m(TreeItem, { 422 | item, 423 | width, 424 | options, 425 | dragOptions, 426 | selectedId: selectedId || state.selectedId, 427 | key: item[id], 428 | }) 429 | ), 430 | canCreate && multipleRoots 431 | ? m( 432 | 'li.mtc__new_root', 433 | { key: -1 }, 434 | m( 435 | '.mtc__item.mtc__clickable', 436 | m( 437 | '.mtc__indent', 438 | m(TreeButton, { 439 | buttonName: 'create', 440 | onclick: () => _createItem(), 441 | }) 442 | ) 443 | ) 444 | ) 445 | : m.fragment({ key: -1 }, ''), 446 | ]) 447 | ) 448 | ); 449 | }, 450 | }; 451 | }; 452 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/tree-item.ts: -------------------------------------------------------------------------------- 1 | import m, { FactoryComponent, Attributes } from 'mithril'; 2 | import { ITreeItem } from './models'; 3 | import { IInternalTreeOptions } from './models/tree-options'; 4 | import { TreeButton } from './utils'; 5 | 6 | export const TreeItemIdPrefix = 'tree-item-'; 7 | 8 | interface ITreeItemAttributes { 9 | width: number; 10 | item: ITreeItem; 11 | options: IInternalTreeOptions; 12 | selectedId?: string | number; 13 | dragOptions: Attributes; 14 | } 15 | 16 | export const TreeItem: FactoryComponent = () => { 17 | const tiState = {} as { 18 | open: (treeItem: ITreeItem, isExpanded: boolean) => void; 19 | toggle: (treeItem: ITreeItem, onToggle?: (treeItem: ITreeItem, isExpanded: boolean) => void) => void; 20 | }; 21 | 22 | return { 23 | oninit: ({ attrs }) => { 24 | const { options } = attrs; 25 | const { isOpen = 'isOpen', id, _hasChildren } = options; 26 | 27 | tiState.toggle = (treeItem: ITreeItem, onToggle?: (treeItem: ITreeItem, isExpanded: boolean) => void) => { 28 | if (_hasChildren(treeItem)) { 29 | if (typeof isOpen === 'function') { 30 | isOpen(treeItem[id], 'set', !isOpen(treeItem[id], 'get')); 31 | } else { 32 | treeItem[isOpen] = !treeItem[isOpen]; 33 | } 34 | onToggle && onToggle(treeItem, typeof isOpen === 'function' ? isOpen(treeItem[id], 'get') : treeItem[isOpen]); 35 | } 36 | }; 37 | tiState.open = (treeItem: ITreeItem, isExpanded: boolean) => { 38 | if (isExpanded) { 39 | return; 40 | } 41 | if (typeof isOpen === 'function') { 42 | isOpen(treeItem[id], 'set', true); 43 | } else { 44 | treeItem[isOpen] = true; 45 | } 46 | }; 47 | }, 48 | view: ({ attrs: { item, options, dragOptions, selectedId, width } }) => { 49 | const { 50 | id, 51 | treeItemView, 52 | _findChildren, 53 | _isExpanded, 54 | _addChildren, 55 | _hasChildren, 56 | _depth, 57 | onSelect, 58 | onToggle, 59 | onDelete, 60 | editable: { canUpdate, canCreate, canDelete, canDeleteParent }, 61 | maxDepth, 62 | } = options; 63 | const { toggle, open } = tiState; 64 | const isExpanded = _isExpanded(item); 65 | const hasChildren = _hasChildren(item); 66 | const depth = _depth(item); 67 | return m( 68 | `li${canUpdate ? '.mtc__draggable' : ''}[id=${TreeItemIdPrefix}${item[id]}][draggable=${canUpdate}]`, 69 | dragOptions, 70 | [ 71 | m( 72 | '.mtc__item', 73 | { 74 | onclick: (ev: MouseEvent) => { 75 | ev.stopPropagation(); 76 | selectedId !== item[id] && onSelect(item, true); 77 | }, 78 | }, 79 | [ 80 | m( 81 | '.mtc__header.mtc__clearfix', 82 | { 83 | class: `${selectedId === item[id] ? 'active' : ''}`, 84 | }, 85 | [ 86 | hasChildren 87 | ? m(TreeButton, { 88 | buttonName: isExpanded ? 'expand_less' : 'expand_more', 89 | onclick: () => toggle(item, onToggle), 90 | }) 91 | : undefined, 92 | m( 93 | '.mtc__item-title', 94 | { 95 | class: `${canUpdate ? 'mtc__moveable' : ''} ${hasChildren ? '' : 'mtc__childless-item'}`, 96 | style: `max-width: ${width}px`, 97 | }, 98 | m(treeItemView, { treeItem: item, depth, width }) 99 | ), 100 | ], 101 | m('.mtc__act-group', [ 102 | canDelete && (canDeleteParent || !hasChildren) 103 | ? m(TreeButton, { 104 | buttonName: 'delete', 105 | onclick: () => onDelete(item), 106 | }) 107 | : '', 108 | canCreate && depth < maxDepth 109 | ? m(TreeButton, { 110 | buttonName: 'add_child', 111 | onclick: (ev: MouseEvent) => { 112 | ev.stopPropagation(); 113 | _addChildren(item, width); 114 | open(item, isExpanded); 115 | }, 116 | }) 117 | : '', 118 | ]) 119 | ), 120 | isExpanded 121 | ? m('ul.mtc__item-body', [ 122 | // ...item[children].map((i: ITreeItem) => 123 | ..._findChildren(item).map((i: ITreeItem) => 124 | m(TreeItem, { 125 | width: width - 12, 126 | item: i, 127 | options, 128 | dragOptions, 129 | selectedId, 130 | key: i[id], 131 | }) 132 | ), 133 | ]) 134 | : '', 135 | ] 136 | ), 137 | ] 138 | ); 139 | }, 140 | }; 141 | }; 142 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import m, { FactoryComponent, Attributes } from 'mithril'; 2 | import { TreeItemAction } from '..'; 3 | 4 | /** 5 | * Create a GUID 6 | * @see https://stackoverflow.com/a/2117523/319711 7 | * 8 | * @returns RFC4122 version 4 compliant GUID 9 | */ 10 | export const uuid4 = () => { 11 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 12 | // tslint:disable-next-line:no-bitwise 13 | const r = (Math.random() * 16) | 0; 14 | // tslint:disable-next-line:no-bitwise 15 | const v = c === 'x' ? r : (r & 0x3) | 0x8; 16 | return v.toString(16); 17 | }); 18 | }; 19 | 20 | /** Move an item in an array from index to another index */ 21 | export const move = (arr: T[], from: number, to: number) => 22 | arr ? arr.splice(to, 0, arr.splice(from, 1)[0]) : undefined; 23 | 24 | export interface ITreeButtonOptions extends Attributes { 25 | buttonName: TreeItemAction; 26 | } 27 | 28 | export const TreeButton: FactoryComponent = () => { 29 | const textSymbol = (buttonName: TreeItemAction) => { 30 | switch (buttonName) { 31 | case 'add_child': 32 | case 'create': 33 | return '✚'; 34 | case 'delete': 35 | return '✖'; 36 | case 'expand_more': 37 | return '▶'; 38 | case 'expand_less': 39 | return '◢'; 40 | } 41 | }; 42 | const classNames = (buttonName: TreeItemAction) => { 43 | switch (buttonName) { 44 | case 'expand_more': 45 | case 'expand_less': 46 | return '.mtc__clickable.mtc__collapse-expand-item'; 47 | default: 48 | return '.mtc__act'; 49 | } 50 | }; 51 | return { 52 | view: ({ attrs: { buttonName, ...params } }) => m(`${classNames(buttonName)}`, params, textSymbol(buttonName)), 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": 5 | "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 6 | "module": 7 | "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | "lib": [ 9 | "dom", 10 | "es5", 11 | "es2015.promise", 12 | "es2017" 13 | ] /* Specify library files to be included in the compilation. */, 14 | // "allowJs": true, /* Allow javascript files to be compiled. */ 15 | // "checkJs": true, /* Report errors in .js files. */ 16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 17 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 18 | "sourceMap": true /* Generates corresponding '.map' file. */, 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist" /* Redirect output structure to the directory. */, 21 | "rootDir": 22 | "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 23 | "removeComments": false, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "skipLibCheck": true, 31 | "strict": true /* Enable all strict type-checking options. */, 32 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | "strictNullChecks": true, /* Enable strict null checks. */ 34 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | }, 66 | "parcelTsPluginOptions": { 67 | // If true type-checking is disabled 68 | "transpileOnly": false 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/mithril-tree-component/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "no-console": false, 7 | "no-debugger": false, 8 | "quotemark": [true, "single"], 9 | "trailing-comma": [ 10 | true, 11 | { 12 | "multiline": { 13 | "objects": "always", 14 | "arrays": "always", 15 | "functions": "never", 16 | "typeLiterals": "ignore" 17 | }, 18 | "esSpecCompliant": true 19 | } 20 | ], 21 | "object-literal-sort-keys": false, 22 | "ordered-imports": false, 23 | "arrow-parens": [false, "ban-single-arg-parens"] 24 | }, 25 | "rulesDirectory": [] 26 | } 27 | --------------------------------------------------------------------------------