From 6d5e06e71f9722a509747b9763481492208da777 Mon Sep 17 00:00:00 2001 From: Daniel Berthereau Date: Mon, 1 Dec 2014 02:47:02 +0100 Subject: [PATCH] Ajaxified administration of simple pages. --- controllers/AjaxController.php | 166 ++++++++++++++++++++ helpers/SimplePageFunctions.php | 94 +++++++++-- views/admin/css/simple-pages.css | 51 ++++++ views/admin/index/browse-hierarchy-page.php | 21 --- views/admin/index/browse-hierarchy.php | 19 ++- views/admin/index/browse-list.php | 19 ++- views/admin/index/browse.php | 41 ++++- views/admin/javascripts/simple-pages.js | 116 ++++++++++++++ views/shared/images/waiting-mini.gif | Bin 0 -> 8706 bytes 9 files changed, 480 insertions(+), 47 deletions(-) create mode 100644 controllers/AjaxController.php create mode 100644 views/admin/css/simple-pages.css delete mode 100644 views/admin/index/browse-hierarchy-page.php create mode 100644 views/admin/javascripts/simple-pages.js create mode 100644 views/shared/images/waiting-mini.gif diff --git a/controllers/AjaxController.php b/controllers/AjaxController.php new file mode 100644 index 0000000..a2d725e --- /dev/null +++ b/controllers/AjaxController.php @@ -0,0 +1,166 @@ +_helper->viewRenderer->setNoRender(true); + + $this->_helper->db->setDefaultModelName('SimplePagesPage'); + } + + /** + * Handle AJAX requests to update a simple page. + */ + public function updateAction() + { + if (!$this->_checkAjax('update')) { + return; + } + + // Handle action. + try { + $status = $this->_getParam('status'); + if (!in_array($status, array('public', 'private'))) { + $this->getResponse()->setHttpResponseCode(400); + return; + } + + $id = (integer) $this->_getParam('id'); + $simplePage = $this->_helper->db->find($id); + if (!$simplePage) { + $this->getResponse()->setHttpResponseCode(400); + return; + } + $simplePage->is_published = ($status == 'public'); + $simplePage->save(); + } catch (Exception $e) { + $this->getResponse()->setHttpResponseCode(500); + } + } + + /** + * Handle AJAX requests to delete a simple page. + */ + public function deleteAction() + { + if (!$this->_checkAjax('delete')) { + return; + } + + // Handle action. + try { + $id = (integer) $this->_getParam('id'); + $simplePage = $this->_helper->db->find($id); + if (!$simplePage) { + $this->getResponse()->setHttpResponseCode(400); + return; + } + $simplePage->delete(); + } catch (Exception $e) { + $this->getResponse()->setHttpResponseCode(500); + } + } + + /** + * Handle AJAX requests to change a list of simple pages. + */ + public function changeAction() + { + if (!$this->_checkAjax('change')) { + return; + } + + // Handle action. + try { + $remove = $this->_getParam('remove'); + $remove = $remove ?: array(); + if (!is_array($remove)) { + $this->getResponse()->setHttpResponseCode(400); + return; + } + + $order = $this->_getParam('order'); + if (!is_array($order) || empty($order)) { + $this->getResponse()->setHttpResponseCode(400); + return; + } + + // Secure and normalize order. + $newOrder = array(); + // Remove root. + unset($order[0]); + foreach ($order as $input) { + $newOrder[(integer) $input['id']] = (integer) $input['parent_id']; + } + + // Delete pages to remove and update order array. + foreach ($remove as $id) { + $id = (integer) $id; + $simplePage = $this->_helper->db->find($id); + if (!$simplePage) { + $this->getResponse()->setHttpResponseCode(400); + return; + } + // Remove deleted pages from new order and attach children to + // new parent. + $newParentId = $newOrder[$id]; + unset($newOrder[$id]); + foreach ($newOrder as &$parentId) { + if ($parentId == $id) { + $parentId = $newParentId; + } + } + $simplePage->delete(); + } + + // Reorder pages if needed. + simple_pages_update_order($newOrder); + } catch (Exception $e) { + $this->getResponse()->setHttpResponseCode(500); + } + } + + /** + * Check AJAX requests. + * + * 400 Bad Request + * 403 Forbidden + * 500 Internal Server Error + * + * @param string $action + */ + protected function _checkAjax($action) + { + // Only allow AJAX requests. + $request = $this->getRequest(); + if (!$request->isXmlHttpRequest()) { + $this->getResponse()->setHttpResponseCode(403); + return false; + } + + // Allow only valid calls. + if ($request->getControllerName() != 'ajax' + || $request->getActionName() != $action + ) { + $this->getResponse()->setHttpResponseCode(400); + return false; + } + + // Allow only allowed users. + if (!is_allowed('SimplePages_Page', $action)) { + $this->getResponse()->setHttpResponseCode(403); + return false; + } + + return true; + } +} diff --git a/helpers/SimplePageFunctions.php b/helpers/SimplePageFunctions.php index b472e62..7a371fe 100644 --- a/helpers/SimplePageFunctions.php +++ b/helpers/SimplePageFunctions.php @@ -126,20 +126,42 @@ function simple_pages_display_breadcrumbs($pageId = null, $seperator=' > ', $inc return $html; } -function simple_pages_display_hierarchy($parentPageId = 0, $partialFilePath = 'index/browse-hierarchy-page.php') +/** + * Recursively list the pages under a page for editing. + * + * @param SimplePage $page A page to list. + * @return string + */ +function simple_pages_edit_page_list($page) { - $html = ''; - $childrenPages = get_db()->getTable('SimplePagesPage')->findChildrenPages($parentPageId); - if (count($childrenPages)) { + $html = '
  • '; + $html .= '
    '; + $html .= sprintf('%s', html_escape(record_url($page)), html_escape($page->title)); + $html .= ' (' . html_escape($page->slug) . ')'; + $html .= ''; + $html .= '
    '; + $html .= __('%1$s on %2$s', + html_escape(metadata($page, 'modified_username')), + html_escape(format_date(metadata($page, 'updated'), Zend_Date::DATETIME_SHORT))); + $html .= '' . __('Delete') . ''; + $html .= '
    '; + + $childrenPages = $page->getChildren(); + if (count($childrenPages)) { $html .= ''; } + $html .= '
  • '; return $html; } @@ -171,4 +193,56 @@ function simple_pages_get_parent_options($page) } } return $valuePairs; -} \ No newline at end of file +} + +/** + * Update orders of all simple pages that have been modified. + */ +function simple_pages_update_order($newOrder) +{ + $db = get_db(); + $table = $db->SimplePagesPage; + + // Pages are ordered by order, then by title, so two passes are needed. + + // First step: update parent if needed. + $sql = "SELECT `id`, `parent_id` FROM `$table` ORDER BY `order` ASC, `title` ASC"; + $currentOrder = $db->fetchPairs($sql); + + foreach ($newOrder as $id => $parentId) { + if ($currentOrder[$id] != $parentId) { + $db->update($table, + array('parent_id' => $parentId), + 'id = ' . $id); + // Update old hierarchy for next step. + $currentOrder[$id] = $parentId; + } + } + + // Second step: update order if needed for each sibling. + // For each parent, check if current children are ordered as new ones. + while (!empty($newOrder)) { + $parentId = reset($newOrder); + + // Get all current and new pages with this parent. + $currentChildren = array_intersect($currentOrder, array($parentId)); + $newChildren = array_intersect($newOrder, array($parentId)); + + // Compare them and update all values if they are different. + // Orders are compared as csv because no function allows to check order. + if (implode(',', array_keys($currentChildren)) != implode(',', array_keys($newChildren))) { + // Order by 10 for easier insert and update of edited pages. + $order = 10; + foreach ($newChildren as $id => $parentId) { + $db->update($table, + array('order' => $order), + 'id = ' . $id); + $order += 10; + } + } + + // Remove filtered keys before loop. + $currentOrder = array_diff_key($currentOrder, $currentChildren); + $newOrder = array_diff_key($newOrder, $newChildren); + } +} diff --git a/views/admin/css/simple-pages.css b/views/admin/css/simple-pages.css new file mode 100644 index 0000000..fcb2832 --- /dev/null +++ b/views/admin/css/simple-pages.css @@ -0,0 +1,51 @@ +.simplepages-list { + padding: 0; + margin: 0; +} +.simplepages-list li { + list-style: none outside none; + padding-left: 10px; + display: inline-block; + font-family: "Arvo",serif; + margin: 0; + padding: 4px 4px 4px 4px; +} +a.simplepage.status { + position:relative; + padding-left: 20px; + cursor: pointer; +} +a.simplepage.toggle-status.public { + background: url('../../../../../application/views/scripts/images/silk-icons/tick.png') no-repeat scroll 0 0 transparent; +} +a.simplepage.toggle-status.private { + background: url('../../../../../application/views/scripts/images/silk-icons/error.png') no-repeat scroll 0 0 transparent; +} +a.simplepage.transmit { + background: url('../../shared/images/waiting-mini.gif') no-repeat scroll 0 0 transparent !important; +} +.action { + color: #338899; + cursor: pointer; +} +.instructions { + font-size:11px; + color: #888; + clear: both; +} +/* override for core's .undo-delete hiding */ +#page-list .undo-delete { + display: inline; +} +#page-list .page ul { + margin-top: 15px; +} +#page-list ul.action-links { + margin-top: 0; +} +#page-list ul.action-links li { + margin-bottom: 0; +} +#page-list ul.action-links li a.edit { + padding-left: 3px; +} diff --git a/views/admin/index/browse-hierarchy-page.php b/views/admin/index/browse-hierarchy-page.php deleted file mode 100644 index 5eb166d..0000000 --- a/views/admin/index/browse-hierarchy-page.php +++ /dev/null @@ -1,21 +0,0 @@ -simplePage != get_view()->simplePage -So when the subsequent helper functions try to get the current simple page, they would not find them, -Unless we explicitly set the current simple page. -If you try to fix this, see simple_pages_display_hierarchy, especially the call to get_view()->partial -*/ -set_current_record('simple_pages_page', $this->simple_pages_page); -?> - -

    - - ()
    - %1$s on %2$s', - html_escape(metadata('simple_pages_page', 'modified_username')), - html_escape(format_date(metadata('simple_pages_page', 'updated'), Zend_Date::DATETIME_SHORT))); ?> -
    - -

    diff --git a/views/admin/index/browse-hierarchy.php b/views/admin/index/browse-hierarchy.php index ead41d4..9543fda 100644 --- a/views/admin/index/browse-hierarchy.php +++ b/views/admin/index/browse-hierarchy.php @@ -1,3 +1,18 @@ +getTable('SimplePagesPage')->findChildrenPages(0, false); +?> +

    - -
    \ No newline at end of file +
    + +
    + diff --git a/views/admin/index/browse-list.php b/views/admin/index/browse-list.php index 8ac5eb9..c539321 100644 --- a/views/admin/index/browse-list.php +++ b/views/admin/index/browse-list.php @@ -1,3 +1,7 @@ +

    @@ -16,17 +20,20 @@ - - () - diff --git a/views/admin/index/browse.php b/views/admin/index/browse.php index 21a09c9..e0ac59d 100644 --- a/views/admin/index/browse.php +++ b/views/admin/index/browse.php @@ -1,27 +1,52 @@ 'simple-pages primary', - 'title' => html_escape(__('Simple Pages | Browse')), - 'content_class' => 'horizontal-nav'); +$currentView = isset($_GET['view']) && $_GET['view'] == 'hierarchy' ? 'hierarchy' : 'list'; + +queue_css_file('simple-pages', 'screen'); +queue_js_file(array('vendor/jquery.nestedSortable', 'simple-pages')); + +$head = array( + 'title' => html_escape(__('Simple Pages | Browse')), + 'bodyclass' => 'simple-pages primary', + 'content_class' => 'horizontal-nav', +); echo head($head); ?> + + + + -

    +

    - + partial('index/browse-hierarchy.php', array('simplePages' => $simple_pages_pages)); ?> partial('index/browse-list.php', array('simplePages' => $simple_pages_pages)); ?> - + + + + + + + diff --git a/views/admin/javascripts/simple-pages.js b/views/admin/javascripts/simple-pages.js new file mode 100644 index 0000000..cabcafe --- /dev/null +++ b/views/admin/javascripts/simple-pages.js @@ -0,0 +1,116 @@ +jQuery(document).ready(function() { + // Handle published / not published from any status. + jQuery('.simplepage.toggle-status').click(function(event) { + event.preventDefault(); + var id = jQuery(this).attr('id'); + var current = jQuery('#' + id); + id = id.substr(id.lastIndexOf('-') + 1); + var ajaxUrl = jQuery(this).attr('href') + '/simple-pages/ajax/update'; + jQuery(this).addClass('transmit'); + if (jQuery(this).hasClass('public')) { + jQuery.post(ajaxUrl, + { + status: 'private', + id: id + }, + function(data) { + current.addClass('private'); + current.removeClass('public'); + current.removeClass('transmit'); + if (current.text() != '') { + current.text(Omeka.messages.simplepages.private); + } + } + ); + } else { + jQuery.post(ajaxUrl, + { + status: 'public', + id: id + }, + function(data) { + current.addClass('public'); + current.removeClass('private'); + current.removeClass('transmit'); + if (current.text() != '') { + current.text(Omeka.messages.simplepages.public); + } + } + ); + } + }); + + // Handle deletion on list page. + jQuery('.simplepage.delete-confirm').click(function(event) { + event.preventDefault(); + if (!confirm(Omeka.messages.simplepages.confirmation)) { + return; + } + var id = jQuery(this).attr('id'); + var current = jQuery('#' + id); + id = id.substr(id.lastIndexOf('-') + 1); + var row = jQuery(this).closest('tr'); + var ajaxUrl = jQuery(this).attr('href') + '/simple-pages/ajax/delete'; + jQuery(this).addClass('transmit'); + jQuery.post(ajaxUrl, + { + id: id + }, + function(data) { + current.removeClass('transmit'); + row.remove(); + } + ); + }); + + // Enable drag and drop sorting for elements on hierarchy page. + jQuery('.sortable').nestedSortable({ + listType: 'ul', + items: 'li.page', + handle: '.sortable-item', + revert: 200, + forcePlaceholderSize: true, + forceHelperSize: true, + toleranceElement: '> div', + placeholder: 'ui-sortable-highlight', + containment: 'document', + maxLevels: 9 + }); + + // Handle deletion on hierarchy page. + jQuery('#page-list .delete-element').click(function(event) { + event.preventDefault(); + var header = jQuery(this).parent(); + if (jQuery(this).hasClass('delete-element')) { + jQuery(this).removeClass('delete-element').addClass('undo-delete'); + header.addClass('deleted'); + } else { + jQuery(this).removeClass('undo-delete').addClass('delete-element'); + header.removeClass('deleted'); + } + }); + + // Handle changes on hierarchy page. + jQuery('.update-pages').click(function(event) { + event.preventDefault(); + var sortable = jQuery('.sortable'); + sortable.nestedSortable({disabled: true}); + var remove = jQuery('#page-list .deleted').parent() + .map(function() {return parseInt(this.id.substr(this.id.lastIndexOf('_') + 1));}) + .get(); + var order = sortable.nestedSortable('toArray', {startDepthCount: 0}); + order = jQuery.map(order, function(value) {return {id: value.item_id, parent_id: value.parent_id};}); + var ajaxUrl = jQuery(this).attr('href') + '/simple-pages/ajax/change'; + jQuery('.update-pages').addClass('transmit red').removeClass('blue'); + jQuery.post(ajaxUrl, + { + remove: remove, + order: order + }, + function(data) { + // TODO Rebuild instead of reload (simply delete removeds). + location.reload(true); + } + ); + }); +}); diff --git a/views/shared/images/waiting-mini.gif b/views/shared/images/waiting-mini.gif new file mode 100644 index 0000000000000000000000000000000000000000..6a00d4ec3b232b9d54fbd74fad79241fb27b7a71 GIT binary patch literal 8706 zcmeH}OH33&5Qc}a1eF*T2qDH8AVP>yVF7V9!~nvI%4=a&d?N}PH4zdIXgr84FXibY z@u&xlN5Kai^{~dHAP0|nfOyoK@gn}S6`FEpd$vc{MB?WvY36=lVO;02doMgV44d;<;s_2Sf*6Y8u)zk2Knxg$t%yrXDDoSa?C-9)HTYcF zKho=3+uE~EU(Kqx(0Zk|qqQyn{H3!UTJ~a6z3)Jvvcm5x%J;n`l6TAh~Xf8?L+@g-#x zoC=K>2KTI8@9mtQnG9AZXO?xlQi^)_rfjSp&uM(tyDu%T;bGP7qQ3py8}w&a4!Coh zUJN#FtRBnq-Pv}tDYxdnr~Unln}NKB2jzXKCASW23Ow;YZhCp^;O6G1r{3Ht89KD} z*mUQ@`PpKO9@ zHt(7}Aax;Fk^&ruVa9}q0IG*J7Klauy_Qp$rT}VU7xOARMO}La=&>T^@z0 zC+EK+=C&gou@nxxV`_4h@5Gy2#^aSxFTp@^(d1Z?4THxLEJAPuq6m&CIv@dHz~Fh1 zJR*-&7=STz9!qmQI{GWdLku>-G#j=eF75J2qMkhBiH?)sfFm5S6b{j`{EB5Z!8FSl z_{DPvDViKk*k+O9?L;=B2&TqH6hDje+%H582KV!NxSx(N*TWgJjVQC+m@(mz$(aBQ zhF#unJ%;1{tED3xF%u5F8|p*F2Sl)X2tDLO(pEeyqa z3>l~@b-BS$AfYkHpCPSSTF9SZxS?JTNU8(|&mgnyN|;^VZauO()%}ie#8Noa@BPn7 zycG<^JOtCM5@MG}PW9xX0EDQ>ALoBV{wPEwu`uW-pd=U;7`6aFm5@0qNqJNx45@?| zAv4)9WR-AnQK^UXvYgz{>*0Q~9pPB1!eRcFE7613iJ@3I!8EIc*yWMakQ_^DU>N*F zEF(&utT_obOA_aiQ-~2rNCpNB!?~g!gBZ~fhB|pHvQaDoxnE}Udd!R?97@7b`V&~} B?+E|^ literal 0 HcmV?d00001