PropertiesPanel.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269
  1. 'use strict';
  2. var escapeHTML = require('./Utils').escapeHTML;
  3. var domify = require('min-dom').domify,
  4. domQuery = require('min-dom').query,
  5. domQueryAll = require('min-dom').queryAll,
  6. domRemove = require('min-dom').remove,
  7. domClasses = require('min-dom').classes,
  8. domClosest = require('min-dom').closest,
  9. domAttr = require('min-dom').attr,
  10. domDelegate = require('min-dom').delegate,
  11. domMatches = require('min-dom').matches;
  12. var forEach = require('lodash/forEach'),
  13. filter = require('lodash/filter'),
  14. get = require('lodash/get'),
  15. keys = require('lodash/keys'),
  16. isEmpty = require('lodash/isEmpty'),
  17. isArray = require('lodash/isArray'),
  18. xor = require('lodash/xor'),
  19. debounce = require('lodash/debounce');
  20. var updateSelection = require('selection-update');
  21. var scrollTabs = require('scroll-tabs').default;
  22. var getBusinessObject = require('bpmn-js/lib/util/ModelUtil').getBusinessObject;
  23. var HIDE_CLASS = 'bpp-hidden';
  24. var DEBOUNCE_DELAY = 300;
  25. function isToggle(node) {
  26. return node.type === 'checkbox' || node.type === 'radio';
  27. }
  28. function isSelect(node) {
  29. return node.type === 'select-one';
  30. }
  31. function isContentEditable(node) {
  32. return domAttr(node, 'contenteditable');
  33. }
  34. function getPropertyPlaceholders(node) {
  35. var selector = 'input[name], textarea[name], [data-value], [contenteditable]';
  36. var placeholders = domQueryAll(selector, node);
  37. if ((!placeholders || !placeholders.length) && domMatches(node, selector)) {
  38. placeholders = [ node ];
  39. }
  40. return placeholders;
  41. }
  42. /**
  43. * Return all active form controls.
  44. * This excludes the invisible controls unless all is true
  45. *
  46. * @param {Element} node
  47. * @param {Boolean} [all=false]
  48. */
  49. function getFormControls(node, all) {
  50. var controls = domQueryAll('input[name], textarea[name], select[name], [contenteditable]', node);
  51. if (!controls || !controls.length) {
  52. controls = domMatches(node, 'option') ? [ node ] : controls;
  53. }
  54. if (!all) {
  55. controls = filter(controls, function(node) {
  56. return !domClosest(node, '.' + HIDE_CLASS);
  57. });
  58. }
  59. return controls;
  60. }
  61. function getFormControlValuesInScope(entryNode) {
  62. var values = {};
  63. var controlNodes = getFormControls(entryNode);
  64. forEach(controlNodes, function(controlNode) {
  65. var value = controlNode.value;
  66. var name = domAttr(controlNode, 'name') || domAttr(controlNode, 'data-name');
  67. // take toggle state into account for radio / checkboxes
  68. if (isToggle(controlNode)) {
  69. if (controlNode.checked) {
  70. if (!domAttr(controlNode, 'value')) {
  71. value = true;
  72. } else {
  73. value = controlNode.value;
  74. }
  75. } else {
  76. value = null;
  77. }
  78. } else
  79. if (isContentEditable(controlNode)) {
  80. value = controlNode.innerText;
  81. }
  82. if (value !== null) {
  83. // return the actual value
  84. // handle serialization in entry provider
  85. // (ie. if empty string should be serialized or not)
  86. values[name] = value;
  87. }
  88. });
  89. return values;
  90. }
  91. /**
  92. * Extract input values from entry node
  93. *
  94. * @param {DOMElement} entryNode
  95. * @returns {Object}
  96. */
  97. function getFormControlValues(entryNode) {
  98. var values;
  99. var listContainer = domQuery('[data-list-entry-container]', entryNode);
  100. if (listContainer) {
  101. values = [];
  102. var listNodes = listContainer.children || [];
  103. forEach(listNodes, function(listNode) {
  104. values.push(getFormControlValuesInScope(listNode));
  105. });
  106. } else {
  107. values = getFormControlValuesInScope(entryNode);
  108. }
  109. return values;
  110. }
  111. /**
  112. * Return true if the given form extracted value equals
  113. * to an old cached version.
  114. *
  115. * @param {Object} value
  116. * @param {Object} oldValue
  117. * @return {Boolean}
  118. */
  119. function valueEqual(value, oldValue) {
  120. if (value && !oldValue) {
  121. return false;
  122. }
  123. var allKeys = keys(value).concat(keys(oldValue));
  124. return allKeys.every(function(key) {
  125. return value[key] === oldValue[key];
  126. });
  127. }
  128. /**
  129. * Return true if the given form extracted value(s)
  130. * equal an old cached version.
  131. *
  132. * @param {Array<Object>|Object} values
  133. * @param {Array<Object>|Object} oldValues
  134. * @return {Boolean}
  135. */
  136. function valuesEqual(values, oldValues) {
  137. if (isArray(values)) {
  138. if (values.length !== oldValues.length) {
  139. return false;
  140. }
  141. return values.every(function(v, idx) {
  142. return valueEqual(v, oldValues[idx]);
  143. });
  144. }
  145. return valueEqual(values, oldValues);
  146. }
  147. /**
  148. * Return a mapping of { id: entry } for all entries in the given groups in the given tabs.
  149. *
  150. * @param {Object} tabs
  151. * @return {Object}
  152. */
  153. function extractEntries(tabs) {
  154. return keyBy(flattenDeep(map(flattenDeep(map(tabs, 'groups')), 'entries')), 'id');
  155. }
  156. /**
  157. * Return a mapping of { id: group } for all groups in the given tabs.
  158. *
  159. * @param {Object} tabs
  160. * @return {Object}
  161. */
  162. function extractGroups(tabs) {
  163. return keyBy(flattenDeep(map(tabs, 'groups')), 'id');
  164. }
  165. /**
  166. * A properties panel implementation.
  167. *
  168. * To use it provide a `propertiesProvider` component that knows
  169. * about which properties to display.
  170. *
  171. * Properties edit state / visibility can be intercepted
  172. * via a custom {@link PropertiesActivator}.
  173. *
  174. * @class
  175. * @constructor
  176. *
  177. * @param {Object} config
  178. * @param {EventBus} eventBus
  179. * @param {Modeling} modeling
  180. * @param {PropertiesProvider} propertiesProvider
  181. * @param {Canvas} canvas
  182. * @param {CommandStack} commandStack
  183. */
  184. function PropertiesPanel(config, eventBus, modeling, propertiesProvider, commandStack, canvas) {
  185. this._eventBus = eventBus;
  186. this._modeling = modeling;
  187. this._commandStack = commandStack;
  188. this._canvas = canvas;
  189. this._propertiesProvider = propertiesProvider;
  190. this._init(config);
  191. }
  192. PropertiesPanel.$inject = [
  193. 'config.propertiesPanel',
  194. 'eventBus',
  195. 'modeling',
  196. 'propertiesProvider',
  197. 'commandStack',
  198. 'canvas'
  199. ];
  200. module.exports = PropertiesPanel;
  201. PropertiesPanel.prototype._init = function(config) {
  202. var canvas = this._canvas,
  203. eventBus = this._eventBus;
  204. var self = this;
  205. /**
  206. * Select the root element once it is added to the canvas
  207. */
  208. eventBus.on('root.added', function(e) {
  209. var element = e.element;
  210. if (isImplicitRoot(element)) {
  211. return;
  212. }
  213. self.update(element);
  214. });
  215. eventBus.on('selection.changed', function(e) {
  216. var newElement = e.newSelection[0];
  217. var rootElement = canvas.getRootElement();
  218. if (isImplicitRoot(rootElement)) {
  219. return;
  220. }
  221. self.update(newElement);
  222. });
  223. // add / update tab-bar scrolling
  224. eventBus.on([
  225. 'propertiesPanel.changed',
  226. 'propertiesPanel.resized'
  227. ], function(event) {
  228. var tabBarNode = domQuery('.bpp-properties-tab-bar', self._container);
  229. if (!tabBarNode) {
  230. return;
  231. }
  232. var scroller = scrollTabs.get(tabBarNode);
  233. if (!scroller) {
  234. // we did not initialize yet, do that
  235. // now and make sure we select the active
  236. // tab on scroll update
  237. scroller = scrollTabs(tabBarNode, {
  238. selectors: {
  239. tabsContainer: '.bpp-properties-tabs-links',
  240. tab: '.bpp-properties-tabs-links li',
  241. ignore: '.bpp-hidden',
  242. active: '.bpp-active'
  243. }
  244. });
  245. scroller.on('scroll', function(newActiveNode, oldActiveNode, direction) {
  246. var linkNode = domQuery('[data-tab-target]', newActiveNode);
  247. var tabId = domAttr(linkNode, 'data-tab-target');
  248. self.activateTab(tabId);
  249. });
  250. }
  251. // react on tab changes and or tabContainer resize
  252. // and make sure the active tab is shown completely
  253. scroller.update();
  254. });
  255. eventBus.on('elements.changed', function(e) {
  256. var current = self._current;
  257. var element = current && current.element;
  258. if (element) {
  259. if (e.elements.indexOf(element) !== -1) {
  260. self.update(element);
  261. }
  262. }
  263. });
  264. eventBus.on('elementTemplates.changed', function() {
  265. var current = self._current;
  266. var element = current && current.element;
  267. if (element) {
  268. self.update(element);
  269. }
  270. });
  271. eventBus.on('diagram.destroy', function() {
  272. self.detach();
  273. });
  274. this._container = domify('<div class="bpp-properties-panel"></div>');
  275. this._bindListeners(this._container);
  276. if (config && config.parent) {
  277. this.attachTo(config.parent);
  278. }
  279. };
  280. PropertiesPanel.prototype.attachTo = function(parentNode) {
  281. if (!parentNode) {
  282. throw new Error('parentNode required');
  283. }
  284. // ensure we detach from the
  285. // previous, old parent
  286. this.detach();
  287. // unwrap jQuery if provided
  288. if (parentNode.get && parentNode.constructor.prototype.jquery) {
  289. parentNode = parentNode.get(0);
  290. }
  291. if (typeof parentNode === 'string') {
  292. parentNode = domQuery(parentNode);
  293. }
  294. var container = this._container;
  295. parentNode.appendChild(container);
  296. this._emit('attach');
  297. };
  298. PropertiesPanel.prototype.detach = function() {
  299. var container = this._container,
  300. parentNode = container.parentNode;
  301. if (!parentNode) {
  302. return;
  303. }
  304. this._emit('detach');
  305. parentNode.removeChild(container);
  306. };
  307. /**
  308. * Select the given tab within the properties panel.
  309. *
  310. * @param {Object|String} tab
  311. */
  312. PropertiesPanel.prototype.activateTab = function(tab) {
  313. var tabId = typeof tab === 'string' ? tab : tab.id;
  314. var current = this._current;
  315. var panelNode = current.panel;
  316. var allTabNodes = domQueryAll('.bpp-properties-tab', panelNode),
  317. allTabLinkNodes = domQueryAll('.bpp-properties-tab-link', panelNode);
  318. forEach(allTabNodes, function(tabNode) {
  319. var currentTabId = domAttr(tabNode, 'data-tab');
  320. domClasses(tabNode).toggle('bpp-active', tabId === currentTabId);
  321. });
  322. forEach(allTabLinkNodes, function(tabLinkNode) {
  323. var tabLink = domQuery('[data-tab-target]', tabLinkNode),
  324. currentTabId = domAttr(tabLink, 'data-tab-target');
  325. domClasses(tabLinkNode).toggle('bpp-active', tabId === currentTabId);
  326. });
  327. };
  328. /**
  329. * Update the DOM representation of the properties panel
  330. */
  331. PropertiesPanel.prototype.update = function(element) {
  332. var current = this._current;
  333. // no actual selection change
  334. var needsCreate = true;
  335. if (typeof element === 'undefined') {
  336. // use RootElement of BPMN diagram to generate properties panel if no element is selected
  337. element = this._canvas.getRootElement();
  338. }
  339. var newTabs = this._propertiesProvider.getTabs(element);
  340. if (current && current.element === element) {
  341. // see if we can reuse the existing panel
  342. needsCreate = this._entriesChanged(current, newTabs);
  343. }
  344. if (needsCreate) {
  345. if (current) {
  346. // get active tab from the existing panel before remove it
  347. var activeTabNode = domQuery('.bpp-properties-tab.bpp-active', current.panel);
  348. var activeTabId;
  349. if (activeTabNode) {
  350. activeTabId = domAttr(activeTabNode, 'data-tab');
  351. }
  352. // remove old panel
  353. domRemove(current.panel);
  354. }
  355. this._current = this._create(element, newTabs);
  356. // activate the saved active tab from the remove panel or the first tab
  357. (activeTabId) ? this.activateTab(activeTabId) : this.activateTab(this._current.tabs[0]);
  358. }
  359. if (this._current) {
  360. // make sure correct tab contents are visible
  361. this._updateActivation(this._current);
  362. }
  363. this._emit('changed');
  364. };
  365. /**
  366. * Returns true if one of two groups has different entries than the other.
  367. *
  368. * @param {Object} current
  369. * @param {Object} newTabs
  370. * @return {Boolean}
  371. */
  372. PropertiesPanel.prototype._entriesChanged = function(current, newTabs) {
  373. var oldEntryIds = keys(current.entries),
  374. newEntryIds = keys(extractEntries(newTabs));
  375. return !isEmpty(xor(oldEntryIds, newEntryIds));
  376. };
  377. PropertiesPanel.prototype._emit = function(event) {
  378. this._eventBus.fire('propertiesPanel.' + event, { panel: this, current: this._current });
  379. };
  380. PropertiesPanel.prototype._bindListeners = function(container) {
  381. var self = this;
  382. // handles a change for a given event
  383. var handleChange = function handleChange(event) {
  384. // see if we handle a change inside a [data-entry] element.
  385. // if not, drop out
  386. var inputNode = event.delegateTarget,
  387. entryNode = domClosest(inputNode, '[data-entry]'),
  388. entryId, entry;
  389. // change from outside a [data-entry] element, simply ignore
  390. if (!entryNode) {
  391. return;
  392. }
  393. entryId = domAttr(entryNode, 'data-entry');
  394. entry = self.getEntry(entryId);
  395. var values = getFormControlValues(entryNode);
  396. if (event.type === 'change') {
  397. // - if the "data-on-change" attribute is present and a value is changed,
  398. // then the associated action is performed.
  399. // - if the associated action returns "true" then an update to the business
  400. // object is done
  401. // - if it does not return "true", then only the DOM content is updated
  402. var onChangeAction = domAttr(inputNode, 'data-on-change');
  403. if (onChangeAction) {
  404. var isEntryDirty = self.executeAction(entry, entryNode, onChangeAction, event);
  405. if (!isEntryDirty) {
  406. return self.update(self._current.element);
  407. }
  408. }
  409. }
  410. self.applyChanges(entry, values, entryNode);
  411. self.updateState(entry, entryNode);
  412. };
  413. // debounce update only elements that are target of key events,
  414. // i.e. INPUT and TEXTAREA. SELECTs will trigger an immediate update anyway.
  415. domDelegate.bind(container, 'input, textarea, [contenteditable]', 'input', debounce(handleChange, DEBOUNCE_DELAY));
  416. domDelegate.bind(container, 'input, textarea, select, [contenteditable]', 'change', handleChange);
  417. // handle key events
  418. domDelegate.bind(container, 'select', 'keydown', function(e) {
  419. // DEL
  420. if (e.keyCode === 46) {
  421. e.stopPropagation();
  422. e.preventDefault();
  423. }
  424. });
  425. domDelegate.bind(container, '[data-action]', 'click', function onClick(event) {
  426. // triggers on all inputs
  427. var inputNode = event.delegateTarget,
  428. entryNode = domClosest(inputNode, '[data-entry]');
  429. var actionId = domAttr(inputNode, 'data-action'),
  430. entryId = domAttr(entryNode, 'data-entry');
  431. var entry = self.getEntry(entryId);
  432. var isEntryDirty = self.executeAction(entry, entryNode, actionId, event);
  433. if (isEntryDirty) {
  434. var values = getFormControlValues(entryNode);
  435. self.applyChanges(entry, values, entryNode);
  436. }
  437. self.updateState(entry, entryNode);
  438. });
  439. function handleInput(event, element) {
  440. // triggers on all inputs
  441. var inputNode = event.delegateTarget;
  442. var entryNode = domClosest(inputNode, '[data-entry]');
  443. // only work on data entries
  444. if (!entryNode) {
  445. return;
  446. }
  447. var eventHandlerId = domAttr(inputNode, 'data-blur'),
  448. entryId = domAttr(entryNode, 'data-entry');
  449. var entry = self.getEntry(entryId);
  450. var isEntryDirty = self.executeAction(entry, entryNode, eventHandlerId, event);
  451. if (isEntryDirty) {
  452. var values = getFormControlValues(entryNode);
  453. self.applyChanges(entry, values, entryNode);
  454. }
  455. self.updateState(entry, entryNode);
  456. }
  457. domDelegate.bind(container, '[data-blur]', 'blur', handleInput, true);
  458. // make tab links interactive
  459. domDelegate.bind(container, '.bpp-properties-tabs-links [data-tab-target]', 'click', function(event) {
  460. event.preventDefault();
  461. var delegateTarget = event.delegateTarget;
  462. var tabId = domAttr(delegateTarget, 'data-tab-target');
  463. // activate tab on link click
  464. self.activateTab(tabId);
  465. });
  466. };
  467. PropertiesPanel.prototype.updateState = function(entry, entryNode) {
  468. this.updateShow(entry, entryNode);
  469. this.updateDisable(entry, entryNode);
  470. };
  471. /**
  472. * Update the visibility of the entry node in the DOM
  473. */
  474. PropertiesPanel.prototype.updateShow = function(entry, node) {
  475. var current = this._current;
  476. if (!current) {
  477. return;
  478. }
  479. var showNodes = domQueryAll('[data-show]', node) || [];
  480. forEach(showNodes, function(showNode) {
  481. var expr = domAttr(showNode, 'data-show');
  482. var fn = get(entry, expr);
  483. if (fn) {
  484. var scope = domClosest(showNode, '[data-scope]') || node;
  485. var shouldShow = fn(current.element, node, showNode, scope) || false;
  486. if (shouldShow) {
  487. domClasses(showNode).remove(HIDE_CLASS);
  488. } else {
  489. domClasses(showNode).add(HIDE_CLASS);
  490. }
  491. }
  492. });
  493. };
  494. /**
  495. * Evaluates a given function. If it returns true, then the
  496. * node is marked as "disabled".
  497. */
  498. PropertiesPanel.prototype.updateDisable = function(entry, node) {
  499. var current = this._current;
  500. if (!current) {
  501. return;
  502. }
  503. var nodes = domQueryAll('[data-disable]', node) || [];
  504. forEach(nodes, function(currentNode) {
  505. var expr = domAttr(currentNode, 'data-disable');
  506. var fn = get(entry, expr);
  507. if (fn) {
  508. var scope = domClosest(currentNode, '[data-scope]') || node;
  509. var shouldDisable = fn(current.element, node, currentNode, scope) || false;
  510. domAttr(currentNode, 'disabled', shouldDisable ? '' : null);
  511. }
  512. });
  513. };
  514. PropertiesPanel.prototype.executeAction = function(entry, entryNode, actionId, event) {
  515. var current = this._current;
  516. if (!current) {
  517. return;
  518. }
  519. var fn = get(entry, actionId);
  520. if (fn) {
  521. var scopeNode = domClosest(event.target, '[data-scope]') || entryNode;
  522. return fn.apply(entry, [ current.element, entryNode, event, scopeNode ]);
  523. }
  524. };
  525. /**
  526. * Apply changes to the business object by executing a command
  527. */
  528. PropertiesPanel.prototype.applyChanges = function(entry, values, containerElement) {
  529. var element = this._current.element;
  530. // ensure we only update the model if we got dirty changes
  531. if (valuesEqual(values, entry.oldValues)) {
  532. return;
  533. }
  534. var command = entry.set(element, values, containerElement);
  535. var commandToExecute;
  536. if (isArray(command)) {
  537. if (command.length) {
  538. commandToExecute = {
  539. cmd: 'properties-panel.multi-command-executor',
  540. context: flattenDeep(command)
  541. };
  542. }
  543. } else {
  544. commandToExecute = command;
  545. }
  546. if (commandToExecute) {
  547. this._commandStack.execute(commandToExecute.cmd, commandToExecute.context || { element : element });
  548. } else {
  549. this.update(element);
  550. }
  551. };
  552. /**
  553. * apply validation errors in the DOM and show or remove an error message near the entry node.
  554. */
  555. PropertiesPanel.prototype.applyValidationErrors = function(validationErrors, entryNode) {
  556. var valid = true;
  557. var controlNodes = getFormControls(entryNode, true);
  558. forEach(controlNodes, function(controlNode) {
  559. var name = domAttr(controlNode, 'name') || domAttr(controlNode, 'data-name');
  560. var error = validationErrors && validationErrors[name];
  561. var errorMessageNode = domQuery('.bpp-error-message', controlNode.parentNode);
  562. if (error) {
  563. valid = false;
  564. if (!errorMessageNode) {
  565. errorMessageNode = domify('<div></div>');
  566. domClasses(errorMessageNode).add('bpp-error-message');
  567. // insert errorMessageNode after controlNode
  568. controlNode.parentNode.insertBefore(errorMessageNode, controlNode.nextSibling);
  569. }
  570. errorMessageNode.textContent = error;
  571. domClasses(controlNode).add('invalid');
  572. } else {
  573. domClasses(controlNode).remove('invalid');
  574. if (errorMessageNode) {
  575. controlNode.parentNode.removeChild(errorMessageNode);
  576. }
  577. }
  578. });
  579. return valid;
  580. };
  581. /**
  582. * Check if the entry contains valid input
  583. */
  584. PropertiesPanel.prototype.validate = function(entry, values, entryNode) {
  585. var self = this;
  586. var current = this._current;
  587. var valid = true;
  588. entryNode = entryNode || domQuery('[data-entry="' + entry.id + '"]', current.panel);
  589. if (values instanceof Array) {
  590. var listContainer = domQuery('[data-list-entry-container]', entryNode),
  591. listEntryNodes = listContainer.children || [];
  592. // create new elements
  593. for (var i = 0; i < values.length; i++) {
  594. var listValue = values[i];
  595. if (entry.validateListItem) {
  596. var validationErrors = entry.validateListItem(current.element, listValue, entryNode, i),
  597. listEntryNode = listEntryNodes[i];
  598. valid = self.applyValidationErrors(validationErrors, listEntryNode) && valid;
  599. }
  600. }
  601. } else {
  602. if (entry.validate) {
  603. this.validationErrors = entry.validate(current.element, values, entryNode);
  604. valid = self.applyValidationErrors(this.validationErrors, entryNode) && valid;
  605. }
  606. }
  607. return valid;
  608. };
  609. PropertiesPanel.prototype.getEntry = function(id) {
  610. return this._current && this._current.entries[id];
  611. };
  612. var flattenDeep = require('lodash/flattenDeep'),
  613. keyBy = require('lodash/keyBy'),
  614. map = require('lodash/map');
  615. PropertiesPanel.prototype._create = function(element, tabs) {
  616. if (!element) {
  617. return null;
  618. }
  619. var containerNode = this._container;
  620. var panelNode = this._createPanel(element, tabs);
  621. containerNode.appendChild(panelNode);
  622. var entries = extractEntries(tabs);
  623. var groups = extractGroups(tabs);
  624. return {
  625. tabs: tabs,
  626. groups: groups,
  627. entries: entries,
  628. element: element,
  629. panel: panelNode
  630. };
  631. };
  632. /**
  633. * Update variable parts of the entry node on element changes.
  634. *
  635. * @param {djs.model.Base} element
  636. * @param {EntryDescriptor} entry
  637. * @param {Object} values
  638. * @param {HTMLElement} entryNode
  639. * @param {Number} idx
  640. */
  641. PropertiesPanel.prototype._bindTemplate = function(element, entry, values, entryNode, idx) {
  642. var eventBus = this._eventBus;
  643. function isPropertyEditable(entry, propertyName) {
  644. return eventBus.fire('propertiesPanel.isPropertyEditable', {
  645. entry: entry,
  646. propertyName: propertyName,
  647. element: element
  648. });
  649. }
  650. var inputNodes = getPropertyPlaceholders(entryNode);
  651. forEach(inputNodes, function(node) {
  652. var name,
  653. newValue,
  654. editable;
  655. // we deal with an input element
  656. if ('value' in node || isContentEditable(node) === 'true') {
  657. name = domAttr(node, 'name') || domAttr(node, 'data-name');
  658. newValue = values[name];
  659. editable = isPropertyEditable(entry, name);
  660. if (editable && entry.editable) {
  661. editable = entry.editable(element, entryNode, node, name, newValue, idx);
  662. }
  663. domAttr(node, 'readonly', editable ? null : '');
  664. domAttr(node, 'disabled', editable ? null : '');
  665. // take full control over setting the value
  666. // and possibly updating the input in entry#setControlValue
  667. if (entry.setControlValue) {
  668. entry.setControlValue(element, entryNode, node, name, newValue, idx);
  669. } else if (isToggle(node)) {
  670. setToggleValue(node, newValue);
  671. } else if (isSelect(node)) {
  672. setSelectValue(node, newValue);
  673. } else {
  674. setInputValue(node, newValue);
  675. }
  676. }
  677. // we deal with some non-editable html element
  678. else {
  679. name = domAttr(node, 'data-value');
  680. newValue = values[name];
  681. if (entry.setControlValue) {
  682. entry.setControlValue(element, entryNode, node, name, newValue, idx);
  683. } else {
  684. setTextValue(node, newValue);
  685. }
  686. }
  687. });
  688. };
  689. // TODO(nikku): WTF freaking name? Change / clarify.
  690. PropertiesPanel.prototype._updateActivation = function(current) {
  691. var self = this;
  692. var eventBus = this._eventBus;
  693. var element = current.element;
  694. function isEntryVisible(entry) {
  695. return eventBus.fire('propertiesPanel.isEntryVisible', {
  696. entry: entry,
  697. element: element
  698. });
  699. }
  700. function isGroupVisible(group, element, groupNode) {
  701. if (typeof group.enabled === 'function') {
  702. return group.enabled(element, groupNode);
  703. } else {
  704. return true;
  705. }
  706. }
  707. function isTabVisible(tab, element) {
  708. if (typeof tab.enabled === 'function') {
  709. return tab.enabled(element);
  710. } else {
  711. return true;
  712. }
  713. }
  714. function toggleVisible(node, visible) {
  715. domClasses(node).toggle(HIDE_CLASS, !visible);
  716. }
  717. // check whether the active tab is visible
  718. // if not: set the first tab as active tab
  719. function checkActiveTabVisibility(node, visible) {
  720. var isActive = domClasses(node).has('bpp-active');
  721. if (!visible && isActive) {
  722. self.activateTab(current.tabs[0]);
  723. }
  724. }
  725. function updateLabel(element, selector, text) {
  726. var labelNode = domQuery(selector, element);
  727. if (!labelNode) {
  728. return;
  729. }
  730. labelNode.textContent = text;
  731. }
  732. var panelNode = current.panel;
  733. forEach(current.tabs, function(tab) {
  734. var tabNode = domQuery('[data-tab=' + tab.id + ']', panelNode);
  735. var tabLinkNode = domQuery('[data-tab-target=' + tab.id + ']', panelNode).parentNode;
  736. var tabVisible = false;
  737. forEach(tab.groups, function(group) {
  738. var groupVisible = false;
  739. var groupNode = domQuery('[data-group=' + group.id + ']', tabNode);
  740. forEach(group.entries, function(entry) {
  741. var entryNode = domQuery('[data-entry="' + entry.id + '"]', groupNode);
  742. var entryVisible = isEntryVisible(entry);
  743. groupVisible = groupVisible || entryVisible;
  744. toggleVisible(entryNode, entryVisible);
  745. var values = 'get' in entry ? entry.get(element, entryNode) : {};
  746. if (values instanceof Array) {
  747. var listEntryContainer = domQuery('[data-list-entry-container]', entryNode);
  748. var existingElements = listEntryContainer.children || [];
  749. for (var i = 0; i < values.length; i++) {
  750. var listValue = values[i];
  751. var listItemNode = existingElements[i];
  752. if (!listItemNode) {
  753. listItemNode = domify(entry.createListEntryTemplate(listValue, i, listEntryContainer));
  754. listEntryContainer.appendChild(listItemNode);
  755. }
  756. domAttr(listItemNode, 'data-index', i);
  757. self._bindTemplate(element, entry, listValue, listItemNode, i);
  758. }
  759. var entriesToRemove = existingElements.length - values.length;
  760. for (var j = 0; j < entriesToRemove; j++) {
  761. // remove orphaned element
  762. listEntryContainer.removeChild(listEntryContainer.lastChild);
  763. }
  764. } else {
  765. self._bindTemplate(element, entry, values, entryNode);
  766. }
  767. // update conditionally visible elements
  768. self.updateState(entry, entryNode);
  769. self.validate(entry, values, entryNode);
  770. // remember initial state for later dirty checking
  771. entry.oldValues = getFormControlValues(entryNode);
  772. });
  773. if (typeof group.label === 'function') {
  774. updateLabel(groupNode, '.group-label', group.label(element, groupNode));
  775. }
  776. groupVisible = groupVisible && isGroupVisible(group, element, groupNode);
  777. tabVisible = tabVisible || groupVisible;
  778. toggleVisible(groupNode, groupVisible);
  779. });
  780. tabVisible = tabVisible && isTabVisible(tab, element);
  781. toggleVisible(tabNode, tabVisible);
  782. toggleVisible(tabLinkNode, tabVisible);
  783. checkActiveTabVisibility(tabNode, tabVisible);
  784. });
  785. // inject elements id into header
  786. updateLabel(panelNode, '[data-label-id]', getBusinessObject(element).id || '');
  787. };
  788. PropertiesPanel.prototype._createPanel = function(element, tabs) {
  789. var self = this;
  790. var panelNode = domify('<div class="bpp-properties"></div>'),
  791. headerNode = domify('<div class="bpp-properties-header">' +
  792. '<div class="label" data-label-id></div>' +
  793. '<div class="search">' +
  794. '<input type="search" placeholder="Search for property" />' +
  795. '<button><span>Search</span></button>' +
  796. '</div>' +
  797. '</div>'),
  798. tabBarNode = domify('<div class="bpp-properties-tab-bar"></div>'),
  799. tabLinksNode = domify('<ul class="bpp-properties-tabs-links"></ul>'),
  800. tabContainerNode = domify('<div class="bpp-properties-tabs-container"></div>');
  801. panelNode.appendChild(headerNode);
  802. forEach(tabs, function(tab, tabIndex) {
  803. if (!tab.id) {
  804. throw new Error('tab must have an id');
  805. }
  806. var tabNode = domify('<div class="bpp-properties-tab" data-tab="' + escapeHTML(tab.id) + '"></div>'),
  807. tabLinkNode = domify('<li class="bpp-properties-tab-link">' +
  808. '<a href data-tab-target="' + escapeHTML(tab.id) + '">' + escapeHTML(tab.label) + '</a>' +
  809. '</li>');
  810. var groups = tab.groups;
  811. forEach(groups, function(group) {
  812. if (!group.id) {
  813. throw new Error('group must have an id');
  814. }
  815. var groupNode = domify('<div class="bpp-properties-group" data-group="' + escapeHTML(group.id) + '">' +
  816. '<span class="group-toggle"></span>' +
  817. '<span class="group-label">' + escapeHTML(group.label) + '</span>' +
  818. '</div>');
  819. // TODO(nre): use event delegation to handle that...
  820. groupNode.querySelector('.group-toggle').addEventListener('click', function(evt) {
  821. domClasses(groupNode).toggle('group-closed');
  822. evt.preventDefault();
  823. evt.stopPropagation();
  824. });
  825. groupNode.addEventListener('click', function(evt) {
  826. if (!evt.defaultPrevented && domClasses(groupNode).has('group-closed')) {
  827. domClasses(groupNode).remove('group-closed');
  828. }
  829. });
  830. forEach(group.entries, function(entry) {
  831. if (!entry.id) {
  832. throw new Error('entry must have an id');
  833. }
  834. var html = entry.html;
  835. if (typeof html === 'string') {
  836. html = domify(html);
  837. }
  838. // unwrap jquery
  839. if (html.get && html.constructor.prototype.jquery) {
  840. html = html.get(0);
  841. }
  842. var entryNode = domify('<div class="bpp-properties-entry" data-entry="' + escapeHTML(entry.id) + '"></div>');
  843. forEach(entry.cssClasses || [], function(cssClass) {
  844. domClasses(entryNode).add(cssClass);
  845. });
  846. entryNode.appendChild(html);
  847. groupNode.appendChild(entryNode);
  848. // update conditionally visible elements
  849. self.updateState(entry, entryNode);
  850. });
  851. tabNode.appendChild(groupNode);
  852. });
  853. tabLinksNode.appendChild(tabLinkNode);
  854. tabContainerNode.appendChild(tabNode);
  855. });
  856. tabBarNode.appendChild(tabLinksNode);
  857. panelNode.appendChild(tabBarNode);
  858. panelNode.appendChild(tabContainerNode);
  859. return panelNode;
  860. };
  861. function setInputValue(node, value) {
  862. var contentEditable = isContentEditable(node);
  863. var oldValue = contentEditable ? node.innerText : node.value;
  864. var selection;
  865. // prevents input fields from having the value 'undefined'
  866. if (value === undefined) {
  867. value = '';
  868. }
  869. if (oldValue === value) {
  870. return;
  871. }
  872. // update selection on undo/redo
  873. if (document.activeElement === node) {
  874. selection = updateSelection(getSelection(node), oldValue, value);
  875. }
  876. if (contentEditable) {
  877. node.innerText = value;
  878. } else {
  879. node.value = value;
  880. }
  881. if (selection) {
  882. setSelection(node, selection);
  883. }
  884. }
  885. function setSelectValue(node, value) {
  886. if (value !== undefined) {
  887. node.value = value;
  888. }
  889. }
  890. function setToggleValue(node, value) {
  891. var nodeValue = node.value;
  892. node.checked = (value === nodeValue) || (!domAttr(node, 'value') && value);
  893. }
  894. function setTextValue(node, value) {
  895. node.textContent = value;
  896. }
  897. function getSelection(node) {
  898. return isContentEditable(node) ? getContentEditableSelection(node) : {
  899. start: node.selectionStart,
  900. end: node.selectionEnd
  901. };
  902. }
  903. function getContentEditableSelection(node) {
  904. var selection = window.getSelection();
  905. var focusNode = selection.focusNode,
  906. focusOffset = selection.focusOffset,
  907. anchorOffset = selection.anchorOffset;
  908. if (!focusNode) {
  909. throw new Error('not selected');
  910. }
  911. // verify we have selection on the current element
  912. if (!node.contains(focusNode)) {
  913. throw new Error('not selected');
  914. }
  915. return {
  916. start: Math.min(focusOffset, anchorOffset),
  917. end: Math.max(focusOffset, anchorOffset)
  918. };
  919. }
  920. function setSelection(node, selection) {
  921. if (isContentEditable(node)) {
  922. setContentEditableSelection(node, selection);
  923. } else {
  924. node.selectionStart = selection.start;
  925. node.selectionEnd = selection.end;
  926. }
  927. }
  928. function setContentEditableSelection(node, selection) {
  929. var focusNode,
  930. domRange,
  931. domSelection;
  932. focusNode = node.firstChild || node,
  933. domRange = document.createRange();
  934. domRange.setStart(focusNode, selection.start);
  935. domRange.setEnd(focusNode, selection.end);
  936. domSelection = window.getSelection();
  937. domSelection.removeAllRanges();
  938. domSelection.addRange(domRange);
  939. }
  940. function isImplicitRoot(element) {
  941. return element.id === '__implicitroot';
  942. }