neo.mjs 6.13.0 → 6.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/apps/covid/neo-config.json +1 -1
  2. package/apps/portal/view/ViewportController.mjs +5 -4
  3. package/apps/portal/view/home/ContentBox.mjs +80 -0
  4. package/apps/portal/view/home/MainContainer.mjs +51 -15
  5. package/apps/portal/view/learn/ContentTreeList.mjs +10 -3
  6. package/apps/portal/view/learn/MainContainerController.mjs +37 -5
  7. package/apps/portal/view/learn/MainContainerModel.mjs +51 -7
  8. package/apps/portal/view/learn/PageContainer.mjs +21 -9
  9. package/apps/sharedcovid/neo-config.json +1 -1
  10. package/examples/form/field/select/MainContainer.mjs +1 -1
  11. package/package.json +3 -3
  12. package/resources/data/deck/learnneo/pages/2023-10-14T19-25-08-153Z.md +16 -1
  13. package/resources/data/deck/learnneo/pages/ComponentsAndContainers.md +180 -0
  14. package/resources/data/deck/learnneo/pages/Config.md +11 -4
  15. package/resources/data/deck/learnneo/pages/DescribingTheUI.md +6 -0
  16. package/resources/data/deck/learnneo/pages/Earthquakes.md +36 -9
  17. package/resources/data/deck/learnneo/pages/Events.md +55 -43
  18. package/resources/data/deck/learnneo/pages/GuideEvents.md +9 -8
  19. package/resources/data/deck/learnneo/pages/References.md +14 -7
  20. package/resources/data/deck/learnneo/pages/TodoList.md +241 -0
  21. package/resources/data/deck/learnneo/pages/WhyNeo-Quick.md +6 -11
  22. package/resources/data/deck/learnneo/tree.json +2 -0
  23. package/resources/scss/src/apps/portal/home/ContentBox.scss +26 -0
  24. package/resources/scss/src/apps/portal/home/MainContainer.scss +4 -12
  25. package/resources/scss/src/apps/portal/learn/MainContainer.scss +0 -7
  26. package/resources/scss/src/apps/portal/learn/PageContainer.scss +35 -0
  27. package/resources/scss/src/apps/portal/learn/PageSectionsPanel.scss +8 -0
  28. package/src/component/Base.mjs +16 -1
  29. package/src/controller/Application.mjs +12 -1
  30. package/src/controller/Base.mjs +15 -4
  31. package/src/controller/Component.mjs +5 -1
  32. package/src/core/Observable.mjs +62 -22
  33. package/src/form/field/Base.mjs +21 -9
  34. package/src/form/field/Select.mjs +166 -117
  35. package/src/form/field/Text.mjs +7 -10
  36. package/src/main/DomEvents.mjs +2 -1
  37. package/src/main/addon/MonacoEditor.mjs +2 -2
  38. package/src/main/addon/Navigator.mjs +27 -24
  39. package/src/selection/ListModel.mjs +13 -14
  40. package/src/selection/Model.mjs +10 -10
  41. package/src/util/HashHistory.mjs +1 -0
  42. package/src/worker/App.mjs +5 -1
  43. package/src/worker/Manager.mjs +14 -7
  44. package/test/components/files/form/field/Select.mjs +6 -4
@@ -0,0 +1,241 @@
1
+ ## HTML Style
2
+
3
+ In case you did not work with neo yet, but come from a more HTML driven ecosystem,
4
+ you could achieve the task in a similar way.
5
+
6
+ <pre data-neo>
7
+ import Component from '../../../../src/component/Base.mjs';
8
+ import NeoArray from '../../../../src/util/Array.mjs';
9
+ import VdomUtil from '../../../../src/util/VDom.mjs';
10
+
11
+ class MainComponent extends Component {
12
+ static config = {
13
+ className: 'Neo.examples.todoList.version1.MainComponent',
14
+ autoMount: true,
15
+ height : 200,
16
+ margin : 10,
17
+ maxWidth : 300,
18
+ style : {border: '1px solid #000', margin: '20px', overflow: 'scroll'},
19
+ width : 300,
20
+
21
+ items: [
22
+ {id: 1, done: true, text: 'Todo Item 1'},
23
+ {id: 2, done: false, text: 'Todo Item 2'},
24
+ {id: 3, done: false, text: 'Todo Item 3'}
25
+ ],
26
+
27
+ inputValue: null,
28
+
29
+ vdom:
30
+ {cn: [
31
+ {tag: 'ol', cn: []},
32
+ {cn: [
33
+ {tag: 'input', cls: ['todo-input'], required: true, style: {marginLeft: '20px'}},
34
+ {tag: 'button', cls: ['todo-add-button'], html : 'Add Item', style: {marginLeft: '1em'}}
35
+ ]}
36
+ ]}
37
+ }
38
+
39
+ construct(config) {
40
+ super.construct(config);
41
+
42
+ let me = this;
43
+
44
+ me.addDomListeners([
45
+ {click: me.onAddButtonClick, delegate: 'todo-add-button'},
46
+ {click: me.onCheckIconClick, delegate: 'todo-item'},
47
+ {input: me.onInputFieldChange, delegate: 'todo-input'}
48
+ ]);
49
+
50
+ me.createItems(me.items || [])
51
+ }
52
+
53
+ createItems(items) {
54
+ let me = this,
55
+ cls;
56
+
57
+ items.forEach(item => {
58
+ cls = ['todo-item'];
59
+
60
+ if (item.done) {
61
+ cls.push('fa', 'fa-check')
62
+ } else {
63
+ cls.push('far', 'fa-square')
64
+ }
65
+
66
+ me.vdom.cn[0].cn.push({
67
+ tag: 'li',
68
+ cn : [
69
+ {tag: 'span', cls, style: {cursor: 'pointer', width: '20px'}},
70
+ {vtype: 'text', html: item.text}
71
+ ]
72
+ });
73
+ });
74
+
75
+ me.update()
76
+ }
77
+
78
+ onAddButtonClick() {
79
+ let me = this;
80
+
81
+ if (me.inputValue) {
82
+ me.createItems([{
83
+ id : null,
84
+ done: false,
85
+ text: me.inputValue
86
+ }])
87
+ }
88
+ }
89
+
90
+ onCheckIconClick(data) {
91
+ let me = this,
92
+ cls = ['far', 'fa-square'],
93
+ oldCls = ['fa', 'fa-check'],
94
+ node = VdomUtil.findVdomChild(me.vdom, data.path[0].id).vdom;
95
+
96
+ if (data.path[0].cls.includes('fa-square')) {
97
+ cls = ['fa', 'fa-check'];
98
+ oldCls = ['far', 'fa-square']
99
+ }
100
+
101
+ NeoArray.remove(node.cls, oldCls);
102
+ NeoArray.add(node.cls, cls);
103
+
104
+ me.update()
105
+ }
106
+
107
+ onInputFieldChange(data) {
108
+ this.inputValue = data.value
109
+ }
110
+ }
111
+
112
+ Neo.setupClass(MainComponent);
113
+ </pre>
114
+
115
+ ## Neo Style
116
+
117
+ content
118
+
119
+ <pre data-neo>
120
+ import Container from '../../../../src/container/Base.mjs';
121
+ import List from '../../../../src/list/Base.mjs';
122
+ import Model from '../../../../src/data/Model.mjs';
123
+ import Store from '../../../../src/data/Store.mjs';
124
+ import TextField from '../../../../src/form/field/Text.mjs';
125
+ import Toolbar from '../../../../src/toolbar/Base.mjs';
126
+
127
+ class TodoListModel extends Model {
128
+ static config = {
129
+ className : 'Neo.examples.todoList.version2.MainModel',
130
+ keyProperty: 'id',
131
+
132
+ fields: [{
133
+ name: 'id',
134
+ type: 'Int'
135
+ }, {
136
+ name: 'done',
137
+ type: 'Boolean'
138
+ }, {
139
+ name: 'text',
140
+ type: 'String'
141
+ }]
142
+ }
143
+ }
144
+
145
+ Neo.setupClass(TodoListModel);
146
+
147
+
148
+ class TodoListStore extends Store {
149
+ static config = {
150
+ className : 'Neo.examples.todoList.version2.TodoListStore',
151
+ keyProperty: 'id',
152
+ model : TodoListModel,
153
+
154
+ data: [
155
+ {id: 1, done: true, text: 'Todo Item 1'},
156
+ {id: 2, done: false, text: 'Todo Item 2'},
157
+ {id: 3, done: false, text: 'Todo Item 3'}
158
+ ],
159
+
160
+ sorters: [{
161
+ property : 'done',
162
+ direction: 'DESC'
163
+ }, {
164
+ property : 'id',
165
+ direction: 'ASC'
166
+ }]
167
+ }
168
+ }
169
+
170
+ Neo.setupClass(TodoListStore);
171
+
172
+
173
+ class MainContainer extends Container {
174
+ static config = {
175
+ className: 'Neo.examples.todoList.version2.MainContainer',
176
+ style : {padding: '20px'},
177
+
178
+ // custom configs
179
+ idCounter: 3,
180
+ store : null
181
+ }
182
+
183
+ construct(config) {
184
+ super.construct(config);
185
+
186
+ let me = this;
187
+
188
+ me.store = Neo.create({
189
+ module: TodoListStore
190
+ });
191
+
192
+ me.items = [{
193
+ module : List,
194
+ displayField : 'text',
195
+ flex : 1,
196
+ store : me.store,
197
+ style : {padding: '5px'},
198
+ useCheckBoxes: true
199
+ }, {
200
+ module: Toolbar,
201
+ flex : 'none',
202
+ dock : 'bottom',
203
+ items : [{
204
+ module : TextField,
205
+ flex : 1,
206
+ labelPosition: 'inline',
207
+ labelText : 'Item Text',
208
+ reference : 'addItemField'
209
+ }, '->', {
210
+ handler : me.onAddButtonClick,
211
+ handlerScope: me,
212
+ scope : me,
213
+ style : {height: '27px', marginLeft: '1em'},
214
+ text : 'Add Item'
215
+ }]
216
+ }];
217
+ }
218
+
219
+ onAddButtonClick() {
220
+ let me = this,
221
+ field = me.down({reference: 'addItemField'}),
222
+ data;
223
+
224
+ if (field.value) {
225
+ me.idCounter++;
226
+
227
+ data = me.store.data;
228
+
229
+ data.push({
230
+ id : me.idCounter,
231
+ done: false,
232
+ text: field.value
233
+ });
234
+
235
+ me.store.data = data
236
+ }
237
+ }
238
+ }
239
+
240
+ Neo.setupClass(MainContainer);
241
+ </pre>
@@ -1,21 +1,16 @@
1
1
  Neo.mjs applications can be written quickly thanks to its syntax, features, and debugging convenience.
2
2
 
3
+ ## Property lifecycle hooks
4
+ Neo.mjs classes let you specify properties in a way that allows code to detect "before" and "after" changes,
5
+ which makes it easy to handle value validation and transformation, and react to changes.
3
6
 
4
- <details>
5
- <summary><h3>Property lifecycle hooks</h3></summary>
6
- Neo.mjs classes let you specify properties in a way that allows code to detect "before" and "after" changes, which makes it easy to handle value validation and transformation, and react to changes.
7
- </details>
8
-
9
- <details>
10
- <summary><h3>Elegant state management</h3></summary>
7
+ ## Elegant state management
11
8
  Neo.mjs has elegant yet powerful state management features that make it easy to create shared, bindable data.
12
9
  For example, if two components are bound to the same property, a change to the property will automatically be
13
10
  applied to both components.
14
- </details>
15
11
 
16
- <details>
17
- <summary><h3>Simple and powerful debugging</h3></summary>
12
+ ## Simple and powerful debugging
18
13
  Debugging is easy because Neo.mjs uses standard JavaScript, the Neo.mjs class config system, and built-in
19
14
  debugging tools. For example, while developing an application you can click on a component, and in the debugger
20
15
  easily inspect the component and update its properties &mdash; these updates are immediately reflected in the running application.
21
- </details>
16
+
@@ -18,10 +18,12 @@
18
18
  {"name": "Tutorials", "parentId": null, "isLeaf": false, "expanded": false, "id": "Tutorials"},
19
19
  {"name": "Rock Scissors Paper", "parentId": "Tutorials", "isLeaf": true, "expanded": false, "id": "RSP", "hidden": true},
20
20
  {"name": "Earthquakes", "parentId": "Tutorials", "isLeaf": true, "expanded": false, "id": "Earthquakes"},
21
+ {"name": "Todo List", "parentId": "Tutorials", "isLeaf": true, "expanded": false, "id": "TodoList"},
21
22
  {"name": "Guides", "parentId": null, "isLeaf": false, "expanded": false, "id": "InDepth"},
22
23
  {"name": "Config", "parentId": "InDepth", "isLeaf": false, "id": "Config"},
23
24
  {"name": "Instance Lifecycle", "parentId": "InDepth", "isLeaf": false, "id": "InstanceLifecycle"},
24
25
  {"name": "User Input (Forms)", "parentId": "InDepth", "isLeaf": false, "id": "Forms"},
26
+ {"name": "Components and Containers", "parentId": "InDepth", "isLeaf": true, "id": "ComponentsAndContainers"},
25
27
  {"name": "Layouts", "parentId": "InDepth", "isLeaf": false, "id": "Layouts"},
26
28
  {"name": "Custom Components", "parentId": "InDepth", "isLeaf": false, "id": "CustomComponents"},
27
29
  {"name": "Events", "parentId": "InDepth", "isLeaf": true, "expanded": false, "id": "GuideEvents"},
@@ -0,0 +1,26 @@
1
+ .portal-content-box {
2
+ border : 1px solid darkgray;
3
+ cursor : pointer;
4
+ color : #000;
5
+ height : 200px;
6
+ margin-top : 2em;
7
+ padding : 0 1em 1em 1em;
8
+ text-decoration: none;
9
+ width : 300px;
10
+
11
+ &:hover {
12
+ background-color: lightblue;
13
+ }
14
+
15
+ &:not(:last-child) {
16
+ margin-right: 3em;
17
+ }
18
+
19
+ .portal-content-box-content {
20
+
21
+ }
22
+
23
+ .portal-content-box-header {
24
+
25
+ }
26
+ }
@@ -1,15 +1,11 @@
1
1
  .newwebsite-viewport {
2
- align-items : center !important;
3
- display : flex !important;
4
- gap : 48px;
5
- justify-content: center !important;
6
- padding : 10% 15% 15% 15%;
2
+ gap : 48px;
3
+ overflow-y: scroll;
4
+ padding : 5%;
7
5
  }
8
6
 
9
7
  .button-group {
10
- display : flex !important;
11
- flex-direction: row-reverse !important;
12
- gap : 8px !important;
8
+ gap: 8px !important;
13
9
  }
14
10
 
15
11
  .neo-h1 {
@@ -26,7 +22,3 @@
26
22
  height : 150px;
27
23
  width : 100%;
28
24
  }
29
-
30
- .get-started-button {
31
- cursor: not-allowed !important;
32
- }
@@ -4,13 +4,6 @@
4
4
  .main-content-splitter {
5
5
  margin: 0;
6
6
  }
7
-
8
- .learn-content-container {
9
- // align-items: center !important;
10
- // padding: 0 3rem;
11
- overflow: scroll;
12
- }
13
-
14
7
  }
15
8
 
16
9
 
@@ -0,0 +1,35 @@
1
+ .learn-content-container {
2
+ // align-items: center !important;
3
+ // padding: 0 3rem;
4
+ overflow: scroll;
5
+
6
+ .content-bottom-toolbar {
7
+ gap : 8px;
8
+ padding: 0 3rem;
9
+
10
+ .neo-button {
11
+ flex : 1 !important;
12
+ height : 75px;
13
+ justify-content: flex-start;
14
+
15
+ .neo-button-text, .neo-button-glyph {
16
+ padding-top: 16px;
17
+ }
18
+ }
19
+
20
+ .content-bottom-toolbar-previous:before {
21
+ content : 'Previous Page';
22
+ font-family : var(--core-fontfamily-sans);
23
+ position : absolute;
24
+ top : var(--cmp-button-spacinghorizontal);
25
+ left : var(--cmp-button-spacinghorizontal);
26
+ }
27
+ .content-bottom-toolbar-next:before {
28
+ content : 'Next Page';
29
+ font-family : var(--core-fontfamily-sans);
30
+ position : absolute;
31
+ top : var(--cmp-button-spacinghorizontal);
32
+ right : var(--cmp-button-spacinghorizontal);
33
+ }
34
+ }
35
+ }
@@ -1,6 +1,14 @@
1
1
  .portal-page-sections-panel.neo-panel {
2
2
  border: none; // reset the default 1px
3
3
 
4
+ @media only screen and (max-width: 1200px) {
5
+ display: none;
6
+ }
7
+
8
+ .neo-list .neo-list-item {
9
+ white-space: normal;
10
+ }
11
+
4
12
  .neo-panel-header-toolbar {
5
13
  border : none; // reset the default 1px
6
14
  border-bottom: 1px solid #f2f2f2;;
@@ -933,6 +933,20 @@ class Base extends CoreBase {
933
933
  this.changeVdomRootKey('width', value)
934
934
  }
935
935
 
936
+ /**
937
+ * Triggered after the windowId config got changed
938
+ * @param {Number|null} value
939
+ * @param {Number|null} oldValue
940
+ * @protected
941
+ */
942
+ afterSetWindowId(value, oldValue) {
943
+ let controller = this.controller;
944
+
945
+ if (controller && value) {
946
+ controller.windowId = value
947
+ }
948
+ }
949
+
936
950
  /**
937
951
  * Triggered after the wrapperCls config got changed
938
952
  * @param {String[]|null} value
@@ -1119,7 +1133,8 @@ class Base extends CoreBase {
1119
1133
 
1120
1134
  if (value) {
1121
1135
  return ClassSystemUtil.beforeSetInstance(value, null, {
1122
- component: this
1136
+ component: this,
1137
+ windowId : this.windowId
1123
1138
  })
1124
1139
  }
1125
1140
 
@@ -20,6 +20,11 @@ class Application extends Base {
20
20
  * @protected
21
21
  */
22
22
  className: 'Neo.controller.Application',
23
+ /**
24
+ * @member {String} ntype='application'
25
+ * @protected
26
+ */
27
+ ntype: 'application',
23
28
  /**
24
29
  * @member {String|null} appThemeFolder=null
25
30
  */
@@ -50,7 +55,11 @@ class Application extends Base {
50
55
  * @member {Boolean} rendering=false
51
56
  * @protected
52
57
  */
53
- rendering: false
58
+ rendering: false,
59
+ /**
60
+ * @member {Number|null} windowId=null
61
+ */
62
+ windowId: null
54
63
  }
55
64
 
56
65
  /**
@@ -66,6 +75,8 @@ class Application extends Base {
66
75
 
67
76
  let me = this;
68
77
 
78
+ me.windowId = Neo.config.windowId;
79
+
69
80
  Neo.apps = Neo.apps || {};
70
81
 
71
82
  Neo.apps[me.name] = me;
@@ -95,12 +95,18 @@ class Base extends CoreBase {
95
95
  /**
96
96
  *
97
97
  */
98
- onConstructed() {
99
- let currentHash = HashHistory.first(),
100
- defaultHash = this.defaultHash;
98
+ async onConstructed() {
99
+ let me = this,
100
+ currentHash = HashHistory.first(),
101
+ defaultHash = me.defaultHash;
102
+
103
+ // get outside the construction chain => a related cmp & vm has to be constructed too
104
+ await me.timeout(1);
101
105
 
102
106
  if (currentHash) {
103
- this.onHashChange(currentHash, null)
107
+ if (currentHash.windowId === me.windowId) {
108
+ await me.onHashChange(currentHash, null)
109
+ }
104
110
  } else {
105
111
  /*
106
112
  * worker.App: onLoadApplication() will push config.hash into the HashHistory with a 5ms delay.
@@ -117,6 +123,11 @@ class Base extends CoreBase {
117
123
  * @param {Object} oldValue
118
124
  */
119
125
  async onHashChange(value, oldValue) {
126
+ // We only want to trigger hash changes for the same browser window (SharedWorker context)
127
+ if (value.windowId !== this.windowId) {
128
+ return
129
+ }
130
+
120
131
  let me = this,
121
132
  counter = 0,
122
133
  hasRouteBeenFound = false,
@@ -30,7 +30,11 @@ class Component extends Base {
30
30
  * @member {Object} references=null
31
31
  * @protected
32
32
  */
33
- references: null
33
+ references: null,
34
+ /**
35
+ * @member {Number|null} windowId=null
36
+ */
37
+ windowId: null
34
38
  }
35
39
 
36
40
  /**
@@ -29,28 +29,46 @@ class Observable extends Base {
29
29
  * @param {Object} [scope]
30
30
  * @param {String} [eventId]
31
31
  * @param {Object} [data]
32
- * @param {Number} [order]
32
+ * @param {Number|String} [order]
33
33
  * @returns {String|null} eventId null in case an object gets passed as the name (multiple ids)
34
34
  */
35
35
  addListener(name, opts, scope, eventId, data, order) {
36
36
  let me = this,
37
+ delay = 0,
37
38
  nameObject = typeof name === 'object',
39
+ once = false,
38
40
  listener, existing, eventConfig;
39
41
 
40
42
  if (nameObject) {
43
+ if (name.hasOwnProperty('delay')) {
44
+ delay = name.delay;
45
+ delete name.delay
46
+ }
47
+
48
+ if (name.hasOwnProperty('once')) {
49
+ once = name.once;
50
+ delete name.once
51
+ }
52
+
41
53
  if (name.hasOwnProperty('scope')) {
42
54
  scope = name.scope;
43
- delete name.scope;
55
+ delete name.scope
44
56
  }
45
57
 
46
58
  Object.entries(name).forEach(([key, value]) => {
47
- me.addListener(key, value, scope);
48
- });
59
+ if (Neo.isObject(value)) {
60
+ me.addListener(key, {delay, once, scope, ...value})
61
+ } else {
62
+ me.addListener(key, {delay, fn: value, once, scope})
63
+ }
64
+ })
49
65
  } else if (typeof opts === 'object') {
50
- scope = scope || opts.scope;
51
- listener = opts.fn;
52
- order = order || opts.order;
66
+ delay = delay || opts.delay;
53
67
  eventId = eventId || opts.eventId;
68
+ listener = opts.fn;
69
+ once = once || opts.once;
70
+ order = order || opts.order;
71
+ scope = scope || opts.scope;
54
72
  } else if (typeof opts === 'function') {
55
73
  listener = opts;
56
74
  } else if (typeof opts === 'string') {
@@ -61,10 +79,12 @@ class Observable extends Base {
61
79
 
62
80
  if (!nameObject) {
63
81
  eventConfig = {
64
- fn: listener,
65
- scope,
66
82
  data,
67
- id: eventId || Neo.getId('event')
83
+ delay,
84
+ fn: listener,
85
+ id: eventId || Neo.getId('event'),
86
+ once,
87
+ scope
68
88
  };
69
89
 
70
90
  if (existing = me.listeners?.[name]) {
@@ -85,10 +105,10 @@ class Observable extends Base {
85
105
  me.listeners[name] = [eventConfig];
86
106
  }
87
107
 
88
- return eventConfig.id;
108
+ return eventConfig.id
89
109
  }
90
110
 
91
- return null;
111
+ return null
92
112
  }
93
113
 
94
114
  /**
@@ -101,11 +121,22 @@ class Observable extends Base {
101
121
  callback(fn, scope=this, args) {
102
122
  if (fn) {
103
123
  const handler = this.resolveCallback(fn, scope);
104
-
105
- handler.fn.apply(handler.scope, args);
124
+ handler.fn.apply(handler.scope, args)
106
125
  }
107
126
  }
108
127
 
128
+ /**
129
+ * Internal helper method for events which use the delay option
130
+ * @param {Object} cb
131
+ * @param {Array} args
132
+ * @param {Number} delay
133
+ */
134
+ delayedCallback(cb, args, delay) {
135
+ setTimeout(() => {
136
+ cb.fn.apply(cb.scope, args)
137
+ }, delay)
138
+ }
139
+
109
140
  /**
110
141
  * @param name
111
142
  */
@@ -113,29 +144,38 @@ class Observable extends Base {
113
144
  let me = this,
114
145
  args = [].slice.call(arguments, 1),
115
146
  listeners = me.listeners,
116
- handler, handlers, i, len;
147
+ delay, handler, handlers, i, len;
117
148
 
118
149
  if (listeners && listeners[name]) {
119
150
  handlers = [...listeners[name]];
120
- len = handlers.length;
151
+ len = handlers.length;
121
152
 
122
153
  for (i = 0; i < len; i++) {
123
154
  handler = handlers[i];
155
+ delay = handler.delay;
124
156
 
125
- // Resolve function name on the scope (oe me), or, if it starts with 'up.'
157
+ // Resolve function name on the scope (or me), or, if it starts with 'up.'
126
158
  // look in the ownership hierarchy from me.
127
159
  const cb = me.resolveCallback(handler.fn, handler.scope || me);
128
160
 
129
- // remove the listener, in case the scope no longer exists
161
+ // remove the listener if the scope no longer exists
130
162
  if (cb.scope && !cb.scope.id) {
131
- listeners[name].splice(i, 1);
163
+ listeners[name].splice(i, 1)
132
164
  } else {
133
165
  if (!me.suspendEvents) {
134
166
  // Object event format. Inject firer reference in as 'source'
135
- if (args.length === 1 && typeof(args[0]) === 'object') {
136
- args[0].source = me.id;
167
+ if (args.length === 1 && Neo.isObject(args[0])) {
168
+ args[0].source = me.id
169
+ }
170
+
171
+ // remove the listener if it has the once flag
172
+ handler.once && listeners[name].splice(i, 1)
173
+
174
+ if (Neo.isNumber(delay) && delay > 0) {
175
+ me.delayedCallback(cb, handler.data ? args.concat(handler.data) : args, delay)
176
+ } else {
177
+ cb.fn.apply(cb.scope, handler.data ? args.concat(handler.data) : args)
137
178
  }
138
- cb.fn.apply(cb.scope, handler.data ? args.concat(handler.data) : args);
139
179
  }
140
180
  }
141
181
  }