features.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /**
  2. * jQuery.fn.sortElements
  3. * --------------
  4. * @param Function comparator:
  5. * Exactly the same behaviour as [1,2,3].sort(comparator)
  6. *
  7. * @param Function getSortable
  8. * A function that should return the element that is
  9. * to be sorted. The comparator will run on the
  10. * current collection, but you may want the actual
  11. * resulting sort to occur on a parent or another
  12. * associated element.
  13. *
  14. * E.g. $('td').sortElements(comparator, function(){
  15. * return this.parentNode;
  16. * })
  17. *
  18. * The <td>'s parent (<tr>) will be sorted instead
  19. * of the <td> itself.
  20. *
  21. * Credit: http://james.padolsey.com/javascript/sorting-elements-with-jquery/
  22. *
  23. */
  24. jQuery.fn.sortElements = (function(){
  25. var sort = [].sort;
  26. return function(comparator, getSortable) {
  27. getSortable = getSortable || function(){return this;};
  28. var placements = this.map(function(){
  29. var sortElement = getSortable.call(this),
  30. parentNode = sortElement.parentNode,
  31. // Since the element itself will change position, we have
  32. // to have some way of storing its original position in
  33. // the DOM. The easiest way is to have a 'flag' node:
  34. nextSibling = parentNode.insertBefore(
  35. document.createTextNode(''),
  36. sortElement.nextSibling
  37. );
  38. return function() {
  39. if (parentNode === this) {
  40. throw new Error(
  41. "You can't sort elements if any one is a descendant of another."
  42. );
  43. }
  44. // Insert before flag:
  45. parentNode.insertBefore(this, nextSibling);
  46. // Remove flag:
  47. parentNode.removeChild(nextSibling);
  48. };
  49. });
  50. return sort.call(this, comparator).each(function(i){
  51. placements[i].call(getSortable.call(this));
  52. });
  53. };
  54. })();
  55. (function ($) {
  56. Drupal.behaviors.features = {
  57. attach: function(context, settings) {
  58. // Features management form
  59. $('table.features:not(.processed)', context).each(function() {
  60. $(this).addClass('processed');
  61. // Check the overridden status of each feature
  62. Drupal.features.checkStatus();
  63. // Add some nicer row hilighting when checkboxes change values
  64. $('input', this).bind('change', function() {
  65. if (!$(this).attr('checked')) {
  66. $(this).parents('tr').removeClass('enabled').addClass('disabled');
  67. }
  68. else {
  69. $(this).parents('tr').addClass('enabled').removeClass('disabled');
  70. }
  71. });
  72. });
  73. // Export form component selector
  74. $('form.features-export-form select.features-select-components:not(.processed)', context).each(function() {
  75. $(this)
  76. .addClass('processed')
  77. .change(function() {
  78. var target = $(this).val();
  79. $('div.features-select').hide();
  80. $('div.features-select-' + target).show();
  81. return false;
  82. }).trigger('change');
  83. });
  84. // Export form machine-readable JS
  85. $('.feature-name:not(.processed)', context).each(function() {
  86. $('.feature-name')
  87. .addClass('processed')
  88. .after(' <small class="feature-module-name-suffix">&nbsp;</small>');
  89. if ($('.feature-module-name').val() === $('.feature-name').val().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/_+/g, '_') || $('.feature-module-name').val() === '') {
  90. $('.feature-module-name').parents('.form-item').hide();
  91. $('.feature-name').bind('keyup change', function() {
  92. var machine = $(this).val().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/_+/g, '_');
  93. if (machine !== '_' && machine !== '') {
  94. $('.feature-module-name').val(machine);
  95. $('.feature-module-name-suffix').empty().append(' Machine name: ' + machine + ' [').append($('<a href="#">'+ Drupal.t('Edit') +'</a>').click(function() {
  96. $('.feature-module-name').parents('.form-item').show();
  97. $('.feature-module-name-suffix').hide();
  98. $('.feature-name').unbind('keyup');
  99. return false;
  100. })).append(']');
  101. }
  102. else {
  103. $('.feature-module-name').val(machine);
  104. $('.feature-module-name-suffix').text('');
  105. }
  106. });
  107. $('.feature-name').keyup();
  108. }
  109. });
  110. //View info dialog
  111. var infoDialog = $('#features-info-file');
  112. if (infoDialog.length != 0) {
  113. infoDialog.dialog({
  114. autoOpen: false,
  115. modal: true,
  116. draggable: false,
  117. resizable: false,
  118. width: 600,
  119. height: 480
  120. });
  121. }
  122. if ((Drupal.settings.features != undefined) && (Drupal.settings.features.info != undefined)) {
  123. $('#features-info-file textarea').val(Drupal.settings.features.info);
  124. $('#features-info-file').dialog('open');
  125. //To be reset by the button click ajax
  126. Drupal.settings.features.info = undefined;
  127. }
  128. // mark any conflicts with a class
  129. if ((Drupal.settings.features != undefined) && (Drupal.settings.features.conflicts != undefined)) {
  130. for (var moduleName in Drupal.settings.features.conflicts) {
  131. moduleConflicts = Drupal.settings.features.conflicts[moduleName];
  132. $('#features-export-wrapper input[type=checkbox]', context).each(function() {
  133. if (!$(this).hasClass('features-checkall')) {
  134. var key = $(this).attr('name');
  135. var matches = key.match(/^([^\[]+)(\[.+\])?\[(.+)\]\[(.+)\]$/);
  136. var component = matches[1];
  137. var item = matches[4];
  138. if ((component in moduleConflicts) && (moduleConflicts[component].indexOf(item) != -1)) {
  139. $(this).parent().addClass('features-conflict');
  140. }
  141. }
  142. });
  143. }
  144. }
  145. function _checkAll(value) {
  146. if (value) {
  147. $('#features-export-wrapper .component-select input[type=checkbox]:visible', context).each(function() {
  148. var move_id = $(this).attr('id');
  149. $(this).click();
  150. $('#'+ move_id).attr('checked', 'checked');
  151. });
  152. }
  153. else {
  154. $('#features-export-wrapper .component-added input[type=checkbox]:visible', context).each(function() {
  155. var move_id = $(this).attr('id');
  156. $('#'+ move_id).removeAttr('checked');
  157. $(this).click();
  158. $('#'+ move_id).removeAttr('checked');
  159. });
  160. }
  161. }
  162. function updateComponentCountInfo(item, section) {
  163. console.log(section);
  164. switch (section) {
  165. case 'select':
  166. var parent = $(item).closest('.features-export-list').siblings('.features-export-component');
  167. $('.component-count', parent).text(function (index, text) {
  168. return +text + 1;
  169. }
  170. );
  171. break;
  172. case 'added':
  173. case 'detected':
  174. var parent = $(item).closest('.features-export-component');
  175. $('.component-count', parent).text(function (index, text) {
  176. return text - 1;
  177. });
  178. }
  179. }
  180. function moveCheckbox(item, section, value) {
  181. updateComponentCountInfo(item, section);
  182. var curParent = item;
  183. if ($(item).hasClass('form-type-checkbox')) {
  184. item = $(item).children('input[type=checkbox]');
  185. }
  186. else {
  187. curParent = $(item).parents('.form-type-checkbox');
  188. }
  189. var newParent = $(curParent).parents('.features-export-parent').find('.form-checkboxes.component-'+section);
  190. $(curParent).detach();
  191. $(curParent).appendTo(newParent);
  192. var list = ['select', 'added', 'detected', 'included'];
  193. for (i in list) {
  194. $(curParent).removeClass('component-' + list[i]);
  195. $(item).removeClass('component-' + list[i]);
  196. }
  197. $(curParent).addClass('component-'+section);
  198. $(item).addClass('component-'+section);
  199. if (value) {
  200. $(item).attr('checked', 'checked');
  201. }
  202. else {
  203. $(item).removeAttr('checked')
  204. }
  205. $(newParent).parent().removeClass('features-export-empty');
  206. // re-sort new list of checkboxes based on labels
  207. $(newParent).find('label').sortElements(
  208. function(a, b){
  209. return $(a).text() > $(b).text() ? 1 : -1;
  210. },
  211. function(){
  212. return this.parentNode;
  213. }
  214. );
  215. }
  216. // provide timer for auto-refresh trigger
  217. var timeoutID = 0;
  218. var inTimeout = 0;
  219. function _triggerTimeout() {
  220. timeoutID = 0;
  221. _updateDetected();
  222. }
  223. function _resetTimeout() {
  224. inTimeout++;
  225. // if timeout is already active, reset it
  226. if (timeoutID != 0) {
  227. window.clearTimeout(timeoutID);
  228. if (inTimeout > 0) inTimeout--;
  229. }
  230. timeoutID = window.setTimeout(_triggerTimeout, 500);
  231. }
  232. function _updateDetected() {
  233. var autodetect = $('#features-autodetect input[type=checkbox]');
  234. if ((autodetect.length > 0) && (!autodetect.is(':checked'))) return;
  235. // query the server for a list of components/items in the feature and update
  236. // the auto-detected items
  237. var items = []; // will contain a list of selected items exported to feature
  238. var components = {}; // contains object of component names that have checked items
  239. $('#features-export-wrapper input[type=checkbox]:checked', context).each(function() {
  240. if (!$(this).hasClass('features-checkall')) {
  241. var key = $(this).attr('name');
  242. var matches = key.match(/^([^\[]+)(\[.+\])?\[(.+)\]\[(.+)\]$/);
  243. components[matches[1]] = matches[1];
  244. if (!$(this).hasClass('component-detected')) {
  245. items.push(key);
  246. }
  247. }
  248. });
  249. var featureName = $('#edit-module-name').val();
  250. if (featureName == '') {
  251. featureName = '*';
  252. }
  253. var url = Drupal.settings.basePath + 'features/ajaxcallback/' + featureName;
  254. var excluded = Drupal.settings.features.excluded;
  255. var postData = {'items': items, 'excluded': excluded};
  256. jQuery.post(url, postData, function(data) {
  257. if (inTimeout > 0) inTimeout--;
  258. // if we have triggered another timeout then don't update with old results
  259. if (inTimeout == 0) {
  260. // data is an object keyed by component listing the exports of the feature
  261. for (var component in data) {
  262. var itemList = data[component];
  263. $('#features-export-wrapper .component-' + component + ' input[type=checkbox]', context).each(function() {
  264. var key = $(this).attr('value');
  265. // first remove any auto-detected items that are no longer in component
  266. if ($(this).hasClass('component-detected')) {
  267. if (!(key in itemList)) {
  268. moveCheckbox(this, 'select', false)
  269. }
  270. }
  271. // next, add any new auto-detected items
  272. else if ($(this).hasClass('component-select')) {
  273. if (key in itemList) {
  274. moveCheckbox(this, 'detected', itemList[key]);
  275. $(this).parent().show(); // make sure it's not hidden from filter
  276. }
  277. }
  278. });
  279. }
  280. // loop over all selected components and check for any that have been completely removed
  281. for (var component in components) {
  282. if ((data == null) || !(component in data)) {
  283. $('#features-export-wrapper .component-' + component + ' input[type=checkbox].component-detected', context).each(function() {
  284. moveCheckbox(this, 'select', false);
  285. });
  286. }
  287. }
  288. }
  289. }, "json");
  290. }
  291. // Handle component selection UI
  292. $('#features-export-wrapper input[type=checkbox]', context).click(function() {
  293. _resetTimeout();
  294. if ($(this).hasClass('component-select')) {
  295. moveCheckbox(this, 'added', true);
  296. }
  297. else if ($(this).hasClass('component-included')) {
  298. moveCheckbox(this, 'added', false);
  299. }
  300. else if ($(this).hasClass('component-added')) {
  301. if ($(this).is(':checked')) {
  302. moveCheckbox(this, 'included', true);
  303. }
  304. else {
  305. moveCheckbox(this, 'select', false);
  306. }
  307. }
  308. });
  309. // Handle select/unselect all
  310. $('#features-filter .features-checkall', context).click(function() {
  311. if ($(this).attr('checked')) {
  312. _checkAll(true);
  313. $(this).next().html(Drupal.t('Deselect all'));
  314. }
  315. else {
  316. _checkAll(false);
  317. $(this).next().html(Drupal.t('Select all'));
  318. }
  319. _resetTimeout();
  320. });
  321. // Handle filtering
  322. // provide timer for auto-refresh trigger
  323. var filterTimeoutID = 0;
  324. var inFilterTimeout = 0;
  325. function _triggerFilterTimeout() {
  326. filterTimeoutID = 0;
  327. _updateFilter();
  328. }
  329. function _resetFilterTimeout() {
  330. inFilterTimeout++;
  331. // if timeout is already active, reset it
  332. if (filterTimeoutID != 0) {
  333. window.clearTimeout(filterTimeoutID);
  334. if (inFilterTimeout > 0) inFilterTimeout--;
  335. }
  336. filterTimeoutID = window.setTimeout(_triggerFilterTimeout, 200);
  337. }
  338. function _updateFilter() {
  339. var filter = $('#features-filter input').val();
  340. var regex = new RegExp(filter, 'i');
  341. // collapse fieldsets
  342. var newState = {};
  343. var currentState = {};
  344. $('#features-export-wrapper fieldset.features-export-component', context).each(function() {
  345. // expand parent fieldset
  346. var section = $(this).attr('id');
  347. currentState[section] = !($(this).hasClass('collapsed'));
  348. if (!(section in newState)) {
  349. newState[section] = false;
  350. }
  351. $(this).find('div.component-select label').each(function() {
  352. if (filter == '') {
  353. if (currentState[section]) {
  354. Drupal.toggleFieldset($('#'+section));
  355. currentState[section] = false;
  356. }
  357. $(this).parent().show();
  358. }
  359. else if ($(this).text().match(regex)) {
  360. $(this).parent().show();
  361. newState[section] = true;
  362. }
  363. else {
  364. $(this).parent().hide();
  365. }
  366. });
  367. });
  368. for (section in newState) {
  369. if (currentState[section] != newState[section]) {
  370. Drupal.toggleFieldset($('#'+section));
  371. }
  372. }
  373. }
  374. $('#features-filter input', context).bind("input", function() {
  375. _resetFilterTimeout();
  376. });
  377. $('#features-filter .features-filter-clear', context).click(function() {
  378. $('#features-filter input').val('');
  379. _updateFilter();
  380. });
  381. // show the filter bar
  382. $('#features-filter', context).removeClass('element-invisible');
  383. }
  384. }
  385. Drupal.features = {
  386. 'checkStatus': function() {
  387. $('table.features tbody tr').not('.processed').filter(':first').each(function() {
  388. var elem = $(this);
  389. $(elem).addClass('processed');
  390. var uri = $(this).find('a.admin-check').attr('href');
  391. if (uri) {
  392. $.get(uri, [], function(data) {
  393. $(elem).find('.admin-loading').hide();
  394. switch (data.storage) {
  395. case 3:
  396. $(elem).find('.admin-rebuilding').show();
  397. break;
  398. case 2:
  399. $(elem).find('.admin-needs-review').show();
  400. break;
  401. case 1:
  402. $(elem).find('.admin-overridden').show();
  403. break;
  404. default:
  405. $(elem).find('.admin-default').show();
  406. break;
  407. }
  408. Drupal.features.checkStatus();
  409. }, 'json');
  410. }
  411. else {
  412. Drupal.features.checkStatus();
  413. }
  414. });
  415. }
  416. };
  417. })(jQuery);