UNPKG

33.8 kBJavaScriptView Raw
1'use strict';
2
3var createDropdown = require('./Utils').createDropdown,
4 escapeHTML = require('./Utils').escapeHTML;
5
6var domify = require('min-dom').domify,
7 domQuery = require('min-dom').query,
8 domQueryAll = require('min-dom').queryAll,
9 domRemove = require('min-dom').remove,
10 domClasses = require('min-dom').classes,
11 domClosest = require('min-dom').closest,
12 domAttr = require('min-dom').attr,
13 domDelegate = require('min-dom').delegate,
14 domMatches = require('min-dom').matches;
15
16var forEach = require('lodash/forEach'),
17 filter = require('lodash/filter'),
18 get = require('lodash/get'),
19 keys = require('lodash/keys'),
20 isEmpty = require('lodash/isEmpty'),
21 isArray = require('lodash/isArray'),
22 isFunction = require('lodash/isFunction'),
23 isObject = require('lodash/isObject'),
24 xor = require('lodash/xor'),
25 debounce = require('lodash/debounce'),
26 flattenDeep = require('lodash/flattenDeep'),
27 keyBy = require('lodash/keyBy'),
28 map = require('lodash/map'),
29 reduce = require('lodash/reduce');
30
31var updateSelection = require('selection-update');
32
33var scrollTabs = require('scroll-tabs').default;
34
35var getBusinessObject = require('bpmn-js/lib/util/ModelUtil').getBusinessObject;
36
37var HIDE_CLASS = 'bpp-hidden';
38var DEBOUNCE_DELAY = 300;
39
40
41function isToggle(node) {
42 return node.type === 'checkbox' || node.type === 'radio';
43}
44
45function isSelect(node) {
46 return node.type === 'select-one';
47}
48
49function isContentEditable(node) {
50 return domAttr(node, 'contenteditable');
51}
52
53function getPropertyPlaceholders(node) {
54 var selector = 'input[name], textarea[name], [data-value], [contenteditable]';
55 var placeholders = domQueryAll(selector, node);
56 if ((!placeholders || !placeholders.length) && domMatches(node, selector)) {
57 placeholders = [ node ];
58 }
59 return placeholders;
60}
61
62/**
63 * Return all active form controls.
64 * This excludes the invisible controls unless all is true
65 *
66 * @param {Element} node
67 * @param {Boolean} [all=false]
68 */
69function getFormControls(node, all) {
70 var controls = domQueryAll('input[name], textarea[name], select[name], [contenteditable]', node);
71
72 if (!controls || !controls.length) {
73 controls = domMatches(node, 'option') ? [ node ] : controls;
74 }
75
76 if (!all) {
77 controls = filter(controls, function(node) {
78 return !domClosest(node, '.' + HIDE_CLASS);
79 });
80 }
81
82 return controls;
83}
84
85function getFormControlValuesInScope(entryNode) {
86 var values = {};
87
88 var controlNodes = getFormControls(entryNode);
89
90 forEach(controlNodes, function(controlNode) {
91 var value = controlNode.value;
92
93 var name = domAttr(controlNode, 'name') || domAttr(controlNode, 'data-name');
94
95 // take toggle state into account for radio / checkboxes
96 if (isToggle(controlNode)) {
97 if (controlNode.checked) {
98 if (!domAttr(controlNode, 'value')) {
99 value = true;
100 } else {
101 value = controlNode.value;
102 }
103 } else {
104 value = null;
105 }
106 } else
107 if (isContentEditable(controlNode)) {
108 value = controlNode.innerText;
109 }
110
111 if (value !== null) {
112 // return the actual value
113 // handle serialization in entry provider
114 // (ie. if empty string should be serialized or not)
115 values[name] = value;
116 }
117 });
118
119 return values;
120
121}
122
123/**
124 * Extract input values from entry node
125 *
126 * @param {DOMElement} entryNode
127 * @returns {Object}
128 */
129function getFormControlValues(entryNode) {
130
131 var values;
132
133 var listContainer = domQuery('[data-list-entry-container]', entryNode);
134 if (listContainer) {
135 values = [];
136 var listNodes = listContainer.children || [];
137 forEach(listNodes, function(listNode) {
138 values.push(getFormControlValuesInScope(listNode));
139 });
140 } else {
141 values = getFormControlValuesInScope(entryNode);
142 }
143
144 return values;
145}
146
147/**
148 * Return true if the given form extracted value equals
149 * to an old cached version.
150 *
151 * @param {Object} value
152 * @param {Object} oldValue
153 * @return {Boolean}
154 */
155function valueEqual(value, oldValue) {
156
157 if (value && !oldValue) {
158 return false;
159 }
160
161 var allKeys = keys(value).concat(keys(oldValue));
162
163 return allKeys.every(function(key) {
164 return value[key] === oldValue[key];
165 });
166}
167
168/**
169 * Return true if the given form extracted value(s)
170 * equal an old cached version.
171 *
172 * @param {Array<Object>|Object} values
173 * @param {Array<Object>|Object} oldValues
174 * @return {Boolean}
175 */
176function valuesEqual(values, oldValues) {
177
178 if (isArray(values)) {
179
180 if (values.length !== oldValues.length) {
181 return false;
182 }
183
184 return values.every(function(v, idx) {
185 return valueEqual(v, oldValues[idx]);
186 });
187 }
188
189 return valueEqual(values, oldValues);
190}
191
192/**
193 * Return a mapping of { id: entry } for all entries in the given groups in the given tabs.
194 *
195 * @param {Object} tabs
196 * @return {Object}
197 */
198function extractEntries(tabs) {
199 return keyBy(flattenDeep(map(flattenDeep(map(tabs, 'groups')), 'entries')), 'id');
200}
201
202/**
203 * Return a mapping of { id: group } for all groups in the given tabs.
204 *
205 * @param {Object} tabs
206 * @return {Object}
207 */
208function extractGroups(tabs) {
209 return keyBy(flattenDeep(map(tabs, 'groups')), 'id');
210}
211
212/**
213 * A properties panel implementation.
214 *
215 * To use it provide a `propertiesProvider` component that knows
216 * about which properties to display.
217 *
218 * Properties edit state / visibility can be intercepted
219 * via a custom {@link PropertiesActivator}.
220 *
221 * @class
222 * @constructor
223 *
224 * @param {Object} config
225 * @param {EventBus} eventBus
226 * @param {Modeling} modeling
227 * @param {PropertiesProvider} propertiesProvider
228 * @param {Canvas} canvas
229 * @param {CommandStack} commandStack
230 */
231function PropertiesPanel(config, eventBus, modeling, propertiesProvider, commandStack, canvas) {
232
233 this._eventBus = eventBus;
234 this._modeling = modeling;
235 this._commandStack = commandStack;
236 this._canvas = canvas;
237 this._propertiesProvider = propertiesProvider;
238
239 this._init(config);
240}
241
242PropertiesPanel.$inject = [
243 'config.propertiesPanel',
244 'eventBus',
245 'modeling',
246 'propertiesProvider',
247 'commandStack',
248 'canvas'
249];
250
251module.exports = PropertiesPanel;
252
253
254PropertiesPanel.prototype._init = function(config) {
255
256 var canvas = this._canvas,
257 eventBus = this._eventBus;
258
259 var self = this;
260
261 /**
262 * Select the root element once it is added to the canvas
263 */
264 eventBus.on('root.added', function(e) {
265 var element = e.element;
266
267 if (isImplicitRoot(element)) {
268 return;
269 }
270
271 self.update(element);
272 });
273
274 eventBus.on('selection.changed', function(e) {
275 var newElement = e.newSelection[0];
276
277 var rootElement = canvas.getRootElement();
278
279 if (isImplicitRoot(rootElement)) {
280 return;
281 }
282
283 self.update(newElement);
284 });
285
286 // add / update tab-bar scrolling
287 eventBus.on([
288 'propertiesPanel.changed',
289 'propertiesPanel.resized'
290 ], function(event) {
291
292 var tabBarNode = domQuery('.bpp-properties-tab-bar', self._container);
293
294 if (!tabBarNode) {
295 return;
296 }
297
298 var scroller = scrollTabs.get(tabBarNode);
299
300 if (!scroller) {
301
302 // we did not initialize yet, do that
303 // now and make sure we select the active
304 // tab on scroll update
305 scroller = scrollTabs(tabBarNode, {
306 selectors: {
307 tabsContainer: '.bpp-properties-tabs-links',
308 tab: '.bpp-properties-tabs-links li',
309 ignore: '.bpp-hidden',
310 active: '.bpp-active'
311 }
312 });
313
314
315 scroller.on('scroll', function(newActiveNode, oldActiveNode, direction) {
316
317 var linkNode = domQuery('[data-tab-target]', newActiveNode);
318
319 var tabId = domAttr(linkNode, 'data-tab-target');
320
321 self.activateTab(tabId);
322 });
323 }
324
325 // react on tab changes and or tabContainer resize
326 // and make sure the active tab is shown completely
327 scroller.update();
328 });
329
330 eventBus.on('elements.changed', function(e) {
331
332 var current = self._current;
333 var element = current && current.element;
334
335 if (element) {
336 if (e.elements.indexOf(element) !== -1) {
337 self.update(element);
338 }
339 }
340 });
341
342 eventBus.on('elementTemplates.changed', function() {
343 var current = self._current;
344 var element = current && current.element;
345
346 if (element) {
347 self.update(element);
348 }
349 });
350
351 eventBus.on('diagram.destroy', function() {
352 self.detach();
353 });
354
355 this._container = domify('<div class="bpp-properties-panel"></div>');
356
357 this._bindListeners(this._container);
358
359 if (config && config.parent) {
360 this.attachTo(config.parent);
361 }
362};
363
364
365PropertiesPanel.prototype.attachTo = function(parentNode) {
366
367 if (!parentNode) {
368 throw new Error('parentNode required');
369 }
370
371 // ensure we detach from the
372 // previous, old parent
373 this.detach();
374
375 // unwrap jQuery if provided
376 if (parentNode.get && parentNode.constructor.prototype.jquery) {
377 parentNode = parentNode.get(0);
378 }
379
380 if (typeof parentNode === 'string') {
381 parentNode = domQuery(parentNode);
382 }
383
384 var container = this._container;
385
386 parentNode.appendChild(container);
387
388 this._emit('attach');
389};
390
391PropertiesPanel.prototype.detach = function() {
392
393 var container = this._container,
394 parentNode = container.parentNode;
395
396 if (!parentNode) {
397 return;
398 }
399
400 this._emit('detach');
401
402 parentNode.removeChild(container);
403};
404
405
406/**
407 * Activate tab. Fall back to first visible tab.
408 *
409 * @param {Object|string} tabId
410 */
411PropertiesPanel.prototype.activateTab = function(tabId) {
412 if (isObject(tabId)) {
413 tabId = tabId.id;
414 }
415
416 var tabs = domQueryAll('.bpp-properties-tab', this._current.panel),
417 tabLinks = domQueryAll('.bpp-properties-tab-link', this._current.panel);
418
419 // (1) Deactivate all tabs
420 forEach(tabs, function(tab) {
421 domClasses(tab).remove('bpp-active');
422 });
423
424 forEach(tabLinks, function(tabLink) {
425 domClasses(tabLink).remove('bpp-active');
426 });
427
428 // (2) Activate tab, fall back to first visible tab
429 var visibleTabs = filter(tabs, function(tab) {
430 return !domClasses(tab).has(HIDE_CLASS);
431 });
432
433 var activeTab = reduce(visibleTabs, function(activeTab, tab) {
434 if (domAttr(tab, 'data-tab') === tabId) {
435 return tab;
436 }
437
438 return activeTab;
439 }, visibleTabs[ 0 ]);
440
441 if (activeTab) {
442 domClasses(activeTab).add('bpp-active');
443 }
444
445 var visibleTabLinks = filter(tabLinks, function(tabLink) {
446 return !domClasses(tabLink).has(HIDE_CLASS);
447 });
448
449 var activeTabLink = reduce(visibleTabLinks, function(activeTabLink, tabLink) {
450 if (domAttr(domQuery('a[data-tab-target]', tabLink), 'data-tab-target') === tabId) {
451 return tabLink;
452 }
453
454 return activeTabLink;
455 }, visibleTabLinks[ 0 ]);
456
457 if (activeTabLink) {
458 domClasses(activeTabLink).add('bpp-active');
459 }
460};
461
462/**
463 * Update properties panel to show properties of element. Create new properties panel if no previous
464 * properties panel, element has changed or element has not changed but entries have.
465 *
466 * @param {ModdleElement|undefined} element
467 */
468PropertiesPanel.prototype.update = function(element) {
469
470 // (1) Fall back to root element
471 if (!element) {
472 element = this._canvas.getRootElement();
473 }
474
475 // (2) Get tabs
476 var tabs = this._propertiesProvider.getTabs(element);
477
478 var activeTabId,
479 activeTabNode;
480
481 if (this._current) {
482 activeTabNode = domQuery('.bpp-properties-tab.bpp-active', this._current.panel);
483
484 if (activeTabNode) {
485 activeTabId = domAttr(activeTabNode, 'data-tab');
486 }
487 }
488
489 // (3) Create new properties panel if necessary
490 if (!this._current
491 || this._current.element !== element
492 || this._entriesChanged(this._current.entries, extractEntries(tabs))) {
493
494 if (this._current) {
495 domRemove(this._current.panel);
496 }
497
498 this._current = this._create(element, tabs);
499 }
500
501 // (4) Update visibility of tabs, groups and entries
502 this._updateActivation(this._current);
503
504 // (5) Update active tab
505 if (activeTabId) {
506 this.activateTab(activeTabId);
507 } else {
508 this.activateTab(this._current.tabs[ 0 ]);
509 }
510
511 this._emit('changed');
512};
513
514
515/**
516 * Check whether entries have changes.
517 *
518 * @param {Array} prevEntries
519 * @param {Array} entries
520 *
521 * @returns {Boolean}
522 */
523PropertiesPanel.prototype._entriesChanged = function(prevEntries, entries) {
524 var prevEntryIds = keys(prevEntries),
525 entryIds = keys(entries);
526
527 return !isEmpty(xor(prevEntryIds, entryIds));
528};
529
530PropertiesPanel.prototype._emit = function(event) {
531 this._eventBus.fire('propertiesPanel.' + event, { panel: this, current: this._current });
532};
533
534PropertiesPanel.prototype._bindListeners = function(container) {
535
536 var self = this;
537
538 // handles a change for a given event
539 var handleChange = function handleChange(event) {
540
541 // see if we handle a change inside a [data-entry] element.
542 // if not, drop out
543 var inputNode = event.delegateTarget,
544 entryNode = domClosest(inputNode, '[data-entry]'),
545 entryId, entry;
546
547 // change from outside a [data-entry] element, simply ignore
548 if (!entryNode) {
549 return;
550 }
551
552 entryId = domAttr(entryNode, 'data-entry');
553 entry = self.getEntry(entryId);
554
555 var values = getFormControlValues(entryNode);
556
557 if (event.type === 'change') {
558
559 // - if the "data-on-change" attribute is present and a value is changed,
560 // then the associated action is performed.
561 // - if the associated action returns "true" then an update to the business
562 // object is done
563 // - if it does not return "true", then only the DOM content is updated
564 var onChangeAction = domAttr(inputNode, 'data-on-change');
565
566 if (onChangeAction) {
567 var isEntryDirty = self.executeAction(entry, entryNode, onChangeAction, event);
568
569 if (!isEntryDirty) {
570 return self.update(self._current.element);
571 }
572 }
573 }
574 self.applyChanges(entry, values, entryNode);
575 self.updateState(entry, entryNode);
576 };
577
578 // debounce update only elements that are target of key events,
579 // i.e. INPUT and TEXTAREA. SELECTs will trigger an immediate update anyway.
580 domDelegate.bind(container, 'input, textarea, [contenteditable]', 'input', debounce(handleChange, DEBOUNCE_DELAY));
581 domDelegate.bind(container, 'input, textarea, select, [contenteditable]', 'change', handleChange);
582
583 // paste as plain text only
584 domDelegate.bind(container, '[contenteditable]', 'paste', handlePaste);
585
586 function handlePaste(event) {
587 var text = (event.clipboardData || window.clipboardData).getData('text');
588 document.execCommand('insertText', false, text);
589
590 event.preventDefault();
591 }
592
593 // handle key events
594 domDelegate.bind(container, 'select', 'keydown', function(e) {
595
596 // DEL
597 if (e.keyCode === 46) {
598 e.stopPropagation();
599 e.preventDefault();
600 }
601 });
602
603 function handleSuggestItems(event) {
604
605 // triggers on all inputs
606 var inputNode = event.delegateTarget;
607
608 var entryNode = domClosest(inputNode, '[data-entry]');
609
610 // only work on data entries
611 if (!entryNode) {
612 return;
613 }
614
615 var action = domAttr(inputNode, 'data-auto-suggest'),
616 entryId = domAttr(entryNode, 'data-entry');
617
618 var entry = self.getEntry(entryId);
619
620 self.executeAction(entry, entryNode, action, event);
621 }
622
623 domDelegate.bind(container, '[data-auto-suggest]', 'input', handleSuggestItems, true);
624
625 domDelegate.bind(container, '[data-action]', 'click', function onClick(event) {
626
627 // triggers on all inputs
628 var inputNode = event.delegateTarget,
629 entryNode = domClosest(inputNode, '[data-entry]');
630
631 var actionId = domAttr(inputNode, 'data-action'),
632 entryId = domAttr(entryNode, 'data-entry');
633
634 var entry = self.getEntry(entryId);
635
636 var isEntryDirty = self.executeAction(entry, entryNode, actionId, event);
637
638 if (!isEntryDirty) {
639 return self.update(self._current.element);
640 }
641
642 var values = getFormControlValues(entryNode);
643
644 self.applyChanges(entry, values, entryNode);
645 self.updateState(entry, entryNode);
646 });
647
648 function handleInput(event, element) {
649 // triggers on all inputs
650 var inputNode = event.delegateTarget;
651
652 var entryNode = domClosest(inputNode, '[data-entry]');
653
654 // only work on data entries
655 if (!entryNode) {
656 return;
657 }
658
659 var eventHandlerId = domAttr(inputNode, 'data-blur'),
660 entryId = domAttr(entryNode, 'data-entry');
661
662 var entry = self.getEntry(entryId);
663
664 var isEntryDirty = self.executeAction(entry, entryNode, eventHandlerId, event);
665
666 if (isEntryDirty) {
667 var values = getFormControlValues(entryNode);
668
669 self.applyChanges(entry, values, entryNode);
670 }
671
672 self.updateState(entry, entryNode);
673 }
674
675 domDelegate.bind(container, '[data-blur]', 'blur', handleInput, true);
676
677 // make tab links interactive
678 domDelegate.bind(container, '.bpp-properties-tabs-links [data-tab-target]', 'click', function(event) {
679 event.preventDefault();
680
681 var delegateTarget = event.delegateTarget;
682
683 var tabId = domAttr(delegateTarget, 'data-tab-target');
684
685 // activate tab on link click
686 self.activateTab(tabId);
687 });
688
689};
690
691PropertiesPanel.prototype.updateState = function(entry, entryNode) {
692 this.updateShow(entry, entryNode);
693 this.updateDisable(entry, entryNode);
694};
695
696/**
697 * Update the visibility of the entry node in the DOM
698 */
699PropertiesPanel.prototype.updateShow = function(entry, node) {
700
701 var current = this._current;
702
703 if (!current) {
704 return;
705 }
706
707 var showNodes = domQueryAll('[data-show]', node) || [];
708
709 forEach(showNodes, function(showNode) {
710
711 var expr = domAttr(showNode, 'data-show');
712 var fn = get(entry, expr);
713 if (fn) {
714 var scope = domClosest(showNode, '[data-scope]') || node;
715 var shouldShow = fn(current.element, node, showNode, scope) || false;
716 if (shouldShow) {
717 domClasses(showNode).remove(HIDE_CLASS);
718 } else {
719 domClasses(showNode).add(HIDE_CLASS);
720 }
721 }
722 });
723};
724
725/**
726 * Evaluates a given function. If it returns true, then the
727 * node is marked as "disabled".
728 */
729PropertiesPanel.prototype.updateDisable = function(entry, node) {
730 var current = this._current;
731
732 if (!current) {
733 return;
734 }
735
736 var nodes = domQueryAll('[data-disable]', node) || [];
737
738 forEach(nodes, function(currentNode) {
739 var expr = domAttr(currentNode, 'data-disable');
740 var fn = get(entry, expr);
741 if (fn) {
742 var scope = domClosest(currentNode, '[data-scope]') || node;
743 var shouldDisable = fn(current.element, node, currentNode, scope) || false;
744 domAttr(currentNode, 'disabled', shouldDisable ? '' : null);
745 }
746 });
747};
748
749PropertiesPanel.prototype.executeAction = function(entry, entryNode, actionId, event) {
750 var current = this._current;
751
752 if (!current) {
753 return;
754 }
755
756 var fn = get(entry, actionId);
757 if (fn) {
758 var scopeNode = domClosest(event.target, '[data-scope]') || entryNode;
759 return fn.apply(entry, [ current.element, entryNode, event, scopeNode ]);
760 }
761};
762
763/**
764 * Apply changes to the business object by executing a command
765 */
766PropertiesPanel.prototype.applyChanges = function(entry, values, containerElement) {
767
768 var element = this._current.element;
769
770 // ensure we only update the model if we got dirty changes
771 if (valuesEqual(values, entry.oldValues)) {
772 return;
773 }
774
775 var command = entry.set(element, values, containerElement);
776
777 var commandToExecute;
778
779 if (isArray(command)) {
780 if (command.length) {
781 commandToExecute = {
782 cmd: 'properties-panel.multi-command-executor',
783 context: flattenDeep(command)
784 };
785 }
786 } else {
787 commandToExecute = command;
788 }
789
790 if (commandToExecute) {
791 this._commandStack.execute(commandToExecute.cmd, commandToExecute.context || { element : element });
792 } else {
793 this.update(element);
794 }
795};
796
797
798/**
799 * apply validation errors in the DOM and show or remove an error message near the entry node.
800 */
801PropertiesPanel.prototype.applyValidationErrors = function(validationErrors, entryNode) {
802
803 var valid = true;
804
805 var controlNodes = getFormControls(entryNode, true);
806
807 forEach(controlNodes, function(controlNode) {
808
809 var name = domAttr(controlNode, 'name') || domAttr(controlNode, 'data-name');
810
811 var error = validationErrors && validationErrors[name];
812
813 var errorMessageNode = domQuery('.bpp-error-message', controlNode.parentNode);
814
815 if (error) {
816 valid = false;
817
818 if (!errorMessageNode) {
819 errorMessageNode = domify('<div></div>');
820
821 domClasses(errorMessageNode).add('bpp-error-message');
822
823 // insert errorMessageNode after controlNode
824 controlNode.parentNode.insertBefore(errorMessageNode, controlNode.nextSibling);
825 }
826
827 errorMessageNode.textContent = error;
828
829 domClasses(controlNode).add('invalid');
830 } else {
831 domClasses(controlNode).remove('invalid');
832
833 if (errorMessageNode) {
834 controlNode.parentNode.removeChild(errorMessageNode);
835 }
836 }
837 });
838
839 return valid;
840};
841
842
843/**
844 * Check if the entry contains valid input
845 */
846PropertiesPanel.prototype.validate = function(entry, values, entryNode) {
847 var self = this;
848
849 var current = this._current;
850
851 var valid = true;
852
853 entryNode = entryNode || domQuery('[data-entry="' + entry.id + '"]', current.panel);
854
855 if (values instanceof Array) {
856 var listContainer = domQuery('[data-list-entry-container]', entryNode),
857 listEntryNodes = listContainer.children || [];
858
859 // create new elements
860 for (var i = 0; i < values.length; i++) {
861 var listValue = values[i];
862
863 if (entry.validateListItem) {
864
865 var validationErrors = entry.validateListItem(current.element, listValue, entryNode, i),
866 listEntryNode = listEntryNodes[i];
867
868 valid = self.applyValidationErrors(validationErrors, listEntryNode) && valid;
869 }
870 }
871 } else {
872 if (entry.validate) {
873 this.validationErrors = entry.validate(current.element, values, entryNode);
874
875 valid = self.applyValidationErrors(this.validationErrors, entryNode) && valid;
876 }
877 }
878
879 return valid;
880};
881
882PropertiesPanel.prototype.getEntry = function(id) {
883 return this._current && this._current.entries[id];
884};
885
886PropertiesPanel.prototype._create = function(element, tabs) {
887
888 if (!element) {
889 return null;
890 }
891
892 var containerNode = this._container;
893
894 var panelNode = this._createPanel(element, tabs);
895
896 containerNode.appendChild(panelNode);
897
898 var entries = extractEntries(tabs);
899 var groups = extractGroups(tabs);
900
901 return {
902 tabs: tabs,
903 groups: groups,
904 entries: entries,
905 element: element,
906 panel: panelNode
907 };
908};
909
910/**
911 * Update variable parts of the entry node on element changes.
912 *
913 * @param {djs.model.Base} element
914 * @param {EntryDescriptor} entry
915 * @param {Object} values
916 * @param {HTMLElement} entryNode
917 * @param {Number} idx
918 */
919PropertiesPanel.prototype._bindTemplate = function(element, entry, values, entryNode, idx) {
920
921 var eventBus = this._eventBus;
922
923 function isPropertyEditable(entry, propertyName) {
924 return eventBus.fire('propertiesPanel.isPropertyEditable', {
925 entry: entry,
926 propertyName: propertyName,
927 element: element
928 });
929 }
930
931 var inputNodes = getPropertyPlaceholders(entryNode);
932
933 forEach(inputNodes, function(node) {
934
935 var name,
936 newValue,
937 editable;
938
939 // we deal with an input element
940 if ('value' in node || isContentEditable(node) === 'true') {
941 name = domAttr(node, 'name') || domAttr(node, 'data-name');
942 newValue = values[name];
943
944 editable = isPropertyEditable(entry, name);
945 if (editable && entry.editable) {
946 editable = entry.editable(element, entryNode, node, name, newValue, idx);
947 }
948
949 domAttr(node, 'readonly', editable ? null : '');
950 domAttr(node, 'disabled', editable ? null : '');
951
952 // take full control over setting the value
953 // and possibly updating the input in entry#setControlValue
954 if (entry.setControlValue) {
955 entry.setControlValue(element, entryNode, node, name, newValue, idx);
956 } else if (isToggle(node)) {
957 setToggleValue(node, newValue);
958 } else if (isSelect(node)) {
959 setSelectValue(node, newValue);
960 } else {
961 setInputValue(node, newValue);
962 }
963 }
964
965 // we deal with some non-editable html element
966 else {
967 name = domAttr(node, 'data-value');
968 newValue = values[name];
969 if (entry.setControlValue) {
970 entry.setControlValue(element, entryNode, node, name, newValue, idx);
971 } else {
972 setTextValue(node, newValue);
973 }
974 }
975 });
976};
977
978// TODO(nikku): WTF freaking name? Change / clarify.
979PropertiesPanel.prototype._updateActivation = function(current) {
980 var self = this;
981
982 var eventBus = this._eventBus;
983
984 var element = current.element;
985
986 function isEntryVisible(entry, group, tab) {
987 return eventBus.fire('propertiesPanel.isEntryVisible', {
988 element: element,
989 entry: entry,
990 group: group,
991 tab: tab
992 });
993 }
994
995 function isGroupVisible(group, element, groupNode) {
996 if (isFunction(group.enabled)) {
997 return group.enabled(element, groupNode);
998 } else {
999 return true;
1000 }
1001 }
1002
1003 function isTabVisible(tab, element) {
1004 if (isFunction(tab.enabled)) {
1005 return tab.enabled(element);
1006 } else {
1007 return true;
1008 }
1009 }
1010
1011 function toggleVisible(node, visible) {
1012 domClasses(node).toggle(HIDE_CLASS, !visible);
1013 }
1014
1015 function updateLabel(element, selector, text) {
1016 var labelNode = domQuery(selector, element);
1017
1018 if (!labelNode) {
1019 return;
1020 }
1021
1022 labelNode.textContent = text;
1023 }
1024
1025 var panelNode = current.panel;
1026
1027 forEach(current.tabs, function(tab) {
1028
1029 var tabNode = domQuery('[data-tab=' + tab.id + ']', panelNode);
1030 var tabLinkNode = domQuery('[data-tab-target=' + tab.id + ']', panelNode).parentNode;
1031
1032 var tabVisible = false;
1033
1034 forEach(tab.groups, function(group) {
1035
1036 var groupVisible = false;
1037
1038 var groupNode = domQuery('[data-group=' + group.id + ']', tabNode);
1039
1040 forEach(group.entries, function(entry) {
1041
1042 var entryNode = domQuery('[data-entry="' + entry.id + '"]', groupNode);
1043
1044 var entryVisible = isEntryVisible(entry, group, tab);
1045
1046 groupVisible = groupVisible || entryVisible;
1047
1048 toggleVisible(entryNode, entryVisible);
1049
1050 var values = 'get' in entry ? entry.get(element, entryNode) : {};
1051
1052 if (values instanceof Array) {
1053 var listEntryContainer = domQuery('[data-list-entry-container]', entryNode);
1054 var existingElements = listEntryContainer.children || [];
1055
1056 for (var i = 0; i < values.length; i++) {
1057 var listValue = values[i];
1058 var listItemNode = existingElements[i];
1059 if (!listItemNode) {
1060 listItemNode = domify(entry.createListEntryTemplate(listValue, i, listEntryContainer));
1061 listEntryContainer.appendChild(listItemNode);
1062 }
1063 domAttr(listItemNode, 'data-index', i);
1064
1065 self._bindTemplate(element, entry, listValue, listItemNode, i);
1066 }
1067
1068 var entriesToRemove = existingElements.length - values.length;
1069
1070 for (var j = 0; j < entriesToRemove; j++) {
1071 // remove orphaned element
1072 listEntryContainer.removeChild(listEntryContainer.lastChild);
1073 }
1074
1075 } else {
1076 self._bindTemplate(element, entry, values, entryNode);
1077 }
1078
1079 // update conditionally visible elements
1080 self.updateState(entry, entryNode);
1081 self.validate(entry, values, entryNode);
1082
1083 // remember initial state for later dirty checking
1084 entry.oldValues = getFormControlValues(entryNode);
1085 });
1086
1087 if (typeof group.label === 'function') {
1088 updateLabel(groupNode, '.group-label', group.label(element, groupNode));
1089 }
1090
1091 groupVisible = groupVisible && isGroupVisible(group, element, groupNode);
1092
1093 tabVisible = tabVisible || groupVisible;
1094
1095 toggleVisible(groupNode, groupVisible);
1096 });
1097
1098 tabVisible = tabVisible && isTabVisible(tab, element);
1099
1100 toggleVisible(tabNode, tabVisible);
1101 toggleVisible(tabLinkNode, tabVisible);
1102 });
1103
1104 // inject elements id into header
1105 updateLabel(panelNode, '[data-label-id]', getBusinessObject(element).id || '');
1106};
1107
1108PropertiesPanel.prototype._createPanel = function(element, tabs) {
1109 var self = this;
1110
1111 var panelNode = domify('<div class="bpp-properties"></div>'),
1112 headerNode = domify('<div class="bpp-properties-header">' +
1113 '<div class="label" data-label-id></div>' +
1114 '</div>'),
1115 tabBarNode = domify('<div class="bpp-properties-tab-bar"></div>'),
1116 tabLinksNode = domify('<ul class="bpp-properties-tabs-links"></ul>'),
1117 tabContainerNode = domify('<div class="bpp-properties-tabs-container"></div>');
1118
1119 panelNode.appendChild(headerNode);
1120
1121 forEach(tabs, function(tab, tabIndex) {
1122
1123 if (!tab.id) {
1124 throw new Error('tab must have an id');
1125 }
1126
1127 var tabNode = domify('<div class="bpp-properties-tab" data-tab="' + escapeHTML(tab.id) + '"></div>'),
1128 tabLinkNode = domify('<li class="bpp-properties-tab-link">' +
1129 '<a href data-tab-target="' + escapeHTML(tab.id) + '">' + escapeHTML(tab.label) + '</a>' +
1130 '</li>');
1131
1132 var groups = tab.groups;
1133
1134 forEach(groups, function(group) {
1135
1136 if (!group.id) {
1137 throw new Error('group must have an id');
1138 }
1139
1140 var groupNode = domify('<div class="bpp-properties-group" data-group="' + escapeHTML(group.id) + '">' +
1141 '<span class="group-toggle"></span>' +
1142 '<div class="group-header">' +
1143 '<span class="group-label">' + escapeHTML(group.label) + '</span>' +
1144 '</div>' +
1145 '</div>');
1146
1147 if (group.dropdown) {
1148 domQuery('.group-header', groupNode).appendChild(createDropdown(group.dropdown));
1149 }
1150
1151 // TODO(nre): use event delegation to handle that...
1152 groupNode.querySelector('.group-toggle').addEventListener('click', function(evt) {
1153 domClasses(groupNode).toggle('group-closed');
1154 evt.preventDefault();
1155 evt.stopPropagation();
1156 });
1157 groupNode.addEventListener('click', function(evt) {
1158 if (!evt.defaultPrevented && domClasses(groupNode).has('group-closed')) {
1159 domClasses(groupNode).remove('group-closed');
1160 }
1161 });
1162
1163 forEach(group.entries, function(entry) {
1164
1165 if (!entry.id) {
1166 throw new Error('entry must have an id');
1167 }
1168
1169 var html = entry.html;
1170
1171 if (typeof html === 'string') {
1172 html = domify(html);
1173 }
1174
1175 // unwrap jquery
1176 if (html.get && html.constructor.prototype.jquery) {
1177 html = html.get(0);
1178 }
1179
1180 var entryNode = domify('<div class="bpp-properties-entry" data-entry="' + escapeHTML(entry.id) + '"></div>');
1181
1182 forEach(entry.cssClasses || [], function(cssClass) {
1183 domClasses(entryNode).add(cssClass);
1184 });
1185
1186 entryNode.appendChild(html);
1187
1188 groupNode.appendChild(entryNode);
1189
1190 // update conditionally visible elements
1191 self.updateState(entry, entryNode);
1192 });
1193
1194 tabNode.appendChild(groupNode);
1195 });
1196
1197 tabLinksNode.appendChild(tabLinkNode);
1198 tabContainerNode.appendChild(tabNode);
1199 });
1200
1201 tabBarNode.appendChild(tabLinksNode);
1202
1203 panelNode.appendChild(tabBarNode);
1204 panelNode.appendChild(tabContainerNode);
1205
1206 return panelNode;
1207};
1208
1209
1210
1211function setInputValue(node, value) {
1212
1213 var contentEditable = isContentEditable(node);
1214
1215 var oldValue = contentEditable ? node.innerText : node.value;
1216
1217 var selection;
1218
1219 // prevents input fields from having the value 'undefined'
1220 if (value === undefined) {
1221 value = '';
1222 }
1223
1224 if (oldValue === value) {
1225 return;
1226 }
1227
1228 // update selection on undo/redo
1229 if (document.activeElement === node) {
1230 selection = updateSelection(getSelection(node), oldValue, value);
1231 }
1232
1233 if (contentEditable) {
1234 node.innerText = value;
1235 } else {
1236 node.value = value;
1237 }
1238
1239 if (selection) {
1240 setSelection(node, selection);
1241 }
1242}
1243
1244function setSelectValue(node, value) {
1245 if (value !== undefined) {
1246 node.value = value;
1247 }
1248}
1249
1250function setToggleValue(node, value) {
1251 var nodeValue = node.value;
1252
1253 node.checked = (value === nodeValue) || (!domAttr(node, 'value') && value);
1254}
1255
1256function setTextValue(node, value) {
1257 node.textContent = value;
1258}
1259
1260function getSelection(node) {
1261
1262 return isContentEditable(node) ? getContentEditableSelection(node) : {
1263 start: node.selectionStart,
1264 end: node.selectionEnd
1265 };
1266}
1267
1268function getContentEditableSelection(node) {
1269
1270 var selection = window.getSelection();
1271
1272 var focusNode = selection.focusNode,
1273 focusOffset = selection.focusOffset,
1274 anchorOffset = selection.anchorOffset;
1275
1276 if (!focusNode) {
1277 throw new Error('not selected');
1278 }
1279
1280 // verify we have selection on the current element
1281 if (!node.contains(focusNode)) {
1282 throw new Error('not selected');
1283 }
1284
1285 return {
1286 start: Math.min(focusOffset, anchorOffset),
1287 end: Math.max(focusOffset, anchorOffset)
1288 };
1289}
1290
1291function setSelection(node, selection) {
1292
1293 if (isContentEditable(node)) {
1294 setContentEditableSelection(node, selection);
1295 } else {
1296 node.selectionStart = selection.start;
1297 node.selectionEnd = selection.end;
1298 }
1299}
1300
1301function setContentEditableSelection(node, selection) {
1302
1303 var focusNode,
1304 domRange,
1305 domSelection;
1306
1307 focusNode = node.firstChild || node,
1308 domRange = document.createRange();
1309 domRange.setStart(focusNode, selection.start);
1310 domRange.setEnd(focusNode, selection.end);
1311
1312 domSelection = window.getSelection();
1313 domSelection.removeAllRanges();
1314 domSelection.addRange(domRange);
1315}
1316
1317function isImplicitRoot(element) {
1318 return element.id === '__implicitroot';
1319}
\No newline at end of file