/ */ class Menu_Module extends P4Cms_Module_Integration { /** * Perform early integration work (before load). */ public static function init() { // install menus when a site is created. P4Cms_PubSub::subscribe('p4cms.site.created', function(P4Cms_Site $site) { $adapter = $site->getStorageAdapter(); P4Cms_Menu::installDefaultMenus(null, $adapter); } ); // update menus when a module/theme is enabled. $installDefaults = function(P4Cms_Site $site, P4Cms_PackageAbstract $package) { $adapter = $site->getStorageAdapter(); P4Cms_Menu::installPackageDefaults($package, null, $adapter); }; P4Cms_PubSub::subscribe('p4cms.site.module.enabled', $installDefaults); P4Cms_PubSub::subscribe('p4cms.site.theme.enabled', $installDefaults); // update menus when a module/theme is disabled. $removeDefaults = function(P4Cms_Site $site, P4Cms_PackageAbstract $package) { $adapter = $site->getStorageAdapter(); P4Cms_Menu::removePackageDefaults($package, $adapter); }; P4Cms_PubSub::subscribe('p4cms.site.module.disabled', $removeDefaults); P4Cms_PubSub::subscribe('p4cms.site.theme.disabled', $removeDefaults); // provide form to search menu management grid P4Cms_PubSub::subscribe('p4cms.menu.grid.form.subForms', function(Zend_Form $form) { return new Ui_Form_GridSearch; } ); // filter menu management grid by keyword search P4Cms_PubSub::subscribe('p4cms.menu.grid.populate', function(P4Cms_Model_Iterator $items, Zend_Form $form) { $values = $form->getValues(); // extract search query. $query = isset($values['search']['query']) ? $values['search']['query'] : null; // early exit if no query. if (!$query) { return null; } // filter categories by the search query $items->search(array('label', 'type'), $query); } ); // provide form to filter by menu P4Cms_PubSub::subscribe('p4cms.menu.grid.form.subForms', function(Zend_Form $form) { $menus = P4Cms_Menu::fetchAll(); // avoid adding the form if no menus are known if (!count($menus)) { return; } $options = array_combine($menus->invoke('getId'), $menus->invoke('getLabel')); $form = new P4Cms_Form_SubForm; $form->setName('menu') ->setAttrib('class', 'types-form') ->setOrder(20) ->addElement( 'MultiCheckbox', 'display', array( 'label' => 'Menu', 'multiOptions' => $options, 'autoApply' => true, 'order' => 20 ) ); return $form; } ); // filter menu management grid by menu P4Cms_PubSub::subscribe('p4cms.menu.grid.populate', function(P4Cms_Model_Iterator $items, Zend_Form $form) { $values = $form->getValues(); // extract search query. $query = isset($values['menu']['display']) ? $values['menu']['display'] : null; // early exit if no query. if (!$query) { return null; } // filter categories by the search query $items->filterByCallback( function($item) use ($query) { return in_array($item->getMenu()->getId(), $query); } ); } ); // provide form to filter by type P4Cms_PubSub::subscribe('p4cms.menu.grid.form.subForms', function(Zend_Form $form) { $types = Menu_Form_MenuItem::getTypeOptions(true, true); $form = new P4Cms_Form_SubForm; $form->setName('type') ->setAttrib('class', 'types-form') ->setOrder(30) ->addElement( 'NestedCheckbox', 'display', array( 'label' => 'Type', 'multiOptions' => $types, 'autoApply' => true, 'order' => 30, 'onClick' => " if (this.value == 'P4Cms_Navigation_Page_Dynamic') { p4cms.ui.toggleChildCheckboxes(this); } else { p4cms.ui.toggleParentCheckbox(this); } " ) ); return $form; } ); // filter menu management grid by type P4Cms_PubSub::subscribe('p4cms.menu.grid.populate', function(P4Cms_Model_Iterator $items, Zend_Form $form) { $values = $form->getValues(); // extract type filter. $types = isset($values['type']['display']) ? $values['type']['display'] : null; // early exit if no types selected. if (!$types || !is_array($types)) { return null; } // modify query to pull out any dynamic types // so we can apply them after primary filtering $dynamicType = 'P4Cms_Navigation_Page_Dynamic'; $dynamicGroup = $dynamicType . '/'; $dynamicTypes = array(); foreach ($types as &$type) { // if they have filtered for top level 'Dynamic' // allow all dynamic types and break if ($type == $dynamicType) { $dynamicTypes = array(); break; } if (strpos($type, $dynamicGroup) === 0) { $dynamicTypes[] = str_replace($dynamicGroup, '', $type); $type = $dynamicType; } } // filter menu items by the search query $items->filter('type', $types); // apply dynamic type filtering if present if (!empty($dynamicTypes)) { $uuids = array(); foreach ($items as $item) { if ($item->hasMenuItem() && in_array($item->getMenuItem()->handler, $dynamicTypes) ) { $uuids[] = $item->getId(); } } $items->filter('id', $uuids); } } ); // provide menu management actions P4Cms_PubSub::subscribe('p4cms.menu.grid.actions', function($actions) { $actions->addPages( array( array( 'label' => 'Edit', 'onClick' => 'p4cms.menu.grid.Actions.onClickEdit();', 'order' => '10' ), array( 'label' => 'Delete', 'onClick' => 'p4cms.menu.grid.Actions.onClickDelete();', 'order' => '20' ), array( 'label' => 'Reset', 'onClick' => 'p4cms.menu.grid.Actions.onClickReset();', 'onShow' => 'p4cms.menu.grid.Actions.onShowReset(this);', 'order' => '30' ), array( 'label' => 'Go To', 'onClick' => 'p4cms.menu.grid.Actions.onClickGoToMenuItem();', 'onShow' => 'p4cms.menu.grid.Actions.onShowGoToMenuItem(this);', 'order' => '40' ), array( 'type' => 'P4Cms_Navigation_Page_Separator', 'order' => '50' ), array( 'label' => 'Add Menu Item', 'onClick' => 'p4cms.menu.grid.Actions.onClickAddMenuItem();', 'order' => '60' ) ) ); } ); // declare page type handlers to influence how different // types of menu items are edited when managing menus. P4Cms_PubSub::subscribe('p4cms.navigation.pageTypeHandlers', function() { $handlers = array(); // declare uri handler. $handler = new P4Cms_Navigation_PageTypeHandler; $handlers[] = $handler; $handler->setId('Zend_Navigation_Page_Uri') ->setLabel('Link') ->setFormCallback( function(Zend_Form $form) { // add a Uri field $form->addElement( 'text', 'uri', array( 'label' => 'Address', 'required' => true, 'filters' => array('StringTrim'), 'size' => 32 ) ); return $form; } ); // declare mvc handler. $handler = new P4Cms_Navigation_PageTypeHandler; $handlers[] = $handler; $handler->setId('P4Cms_Navigation_Page_Mvc') ->setLabel('Action') ->setFormCallback( function(Zend_Form $form) { return new Menu_Form_MenuItemMvc; } ); // declare heading handler. $handler = new P4Cms_Navigation_PageTypeHandler; $handlers[] = $handler; $handler->setId('P4Cms_Navigation_Page_Heading') ->setLabel('Heading') ->setFormCallback( function(Zend_Form $form) { $form->removeElement('onClick'); return $form; } ); // declare separator handler. $handler = new P4Cms_Navigation_PageTypeHandler; $handlers[] = $handler; $handler->setId('P4Cms_Navigation_Page_Separator') ->setLabel('Separator') ->setFormCallback( function(Zend_Form $form) { $form->removeElement('label'); $form->removeElement('target'); $form->removeElement('onClick'); return $form; } ); // declare dynamic handler. $handler = new P4Cms_Navigation_PageTypeHandler; $handlers[] = $handler; $handler->setId('P4Cms_Navigation_Page_Dynamic') ->setLabel('Dynamic') ->setFormCallback( function(Zend_Form $form) { $form->removeElement('title'); $form->removeElement('label'); // borrow elements from widget form. $widgetForm = new Menu_Form_Widget; $form->addElement($widgetForm->getElement('maxDepth')); $form->addElement($widgetForm->getElement('maxItems')); // different dynamic menu item types like different forms. // give dynamic handlers a chance to prepare the form. $dynamicType = $form->getValue('handler'); try { if ($dynamicType) { $dynamicHandler = P4Cms_Navigation_DynamicHandler::fetch($dynamicType); $dynamicHandler->prepareForm($form); } } catch (P4Cms_Model_NotFoundException $e) { // no such dynamic type. we're ok with that. } return $form; } ); // declare content handler. $handler = new P4Cms_Navigation_PageTypeHandler; $handlers[] = $handler; $handler->setId('P4Cms_Navigation_Page_Content') ->setLabel('Content') ->setFormCallback( function(Zend_Form $form) { return new Menu_Form_MenuItemContent; } ); return $handlers; } ); // participate in content editing by providing a subform. P4Cms_PubSub::subscribe('p4cms.content.form.subForms', function(Content_Form_Content $form) { // only show menu sub-form if user has permission $user = P4Cms_User::fetchActive(); if (!$user->isAllowed('menus', 'manage-via-content')) { return; } // add menu form so users can easily add content to menus. return new Menu_Form_Content( array( 'name' => 'menus', 'idPrefix' => $form->getIdPrefix(), 'order' => -40 ) ); } ); // let users add/edit menu items when editing content. P4Cms_PubSub::subscribe('p4cms.content.form.populate', function(Content_Form_Content $form, array $values) { // nothing to do if no menu sub-form $menuForm = $form->getSubForm('menus'); if (!$menuForm) { return; } // pull menus from storage if not in values array. if (isset($values['menus'])) { $items = $values['menus']; } else { $entry = $form->getEntry(); $items = array(); foreach (P4Cms_Menu::fetchMixed() as $mixed) { $item = $mixed->getMenuItem(); if ($item instanceof P4Cms_Navigation_Page_Content && $item->contentId == $entry->getId() ) { $items[] = array_merge( array('menuId' => $mixed->getMenu()->getId()), $item->toArray() ); } } } // for each menu item, add a menu item sub-form. $count = 0; foreach ($items as $item) { $itemForm = $menuForm->getItemForm($item); $menuForm->addSubForm($itemForm, $count++); } } ); // participate in content editing - don't store menus items // on content directly, store them in menu records instead. P4Cms_PubSub::subscribe('p4cms.content.record.preSave', function(P4Cms_Content $entry) { // move menus out of fields list and into a public // property so it doesn't get saved with the record if ($entry->hasField('menus')) { $menus = $entry->getValue('menus'); $entry->unsetValue('menus'); $entry->menus = $menus; } } ); P4Cms_PubSub::subscribe('p4cms.content.record.postSave', function(P4Cms_Content $entry) { // only modify menus if user has adequate permission. $user = P4Cms_User::fetchActive(); if (!$user->isAllowed('menus', 'manage-via-content')) { return; } $adapter = $entry->getAdapter(); $menuItems = is_array($entry->menus) ? $entry->menus : array(); // when we fetch menus, we hang onto them because we are in a // batch and if we re-fetch them we'll lose any earlier changes. $menus = array(); $fetchMenu = function($id) use ($adapter, &$menus) { if (!isset($menus[$id])) { try { $menus[$id] = P4Cms_Menu::fetch($id, null, $adapter); } catch (P4Cms_Model_NotFoundException $e) { return null; } } return $menus[$id]; }; // content entries can have multiple menu entries, loop over // each posted menu entry and add/update/delete as appropriate. foreach ($menuItems as $values) { $form = new Menu_Form_MenuItemContent; // normalize values to always contain the expected elements // and have the correct page type and content id. $values = array_merge( $form->getValues(), array( 'type' => 'P4Cms_Navigation_Page_Content', 'contentId' => $entry->getId() ), $values ); // fetch the menu and the menu item if we're editing. $menu = null; $item = null; if ($values['menuId'] && $values['uuid']) { $menu = $fetchMenu($values['menuId']); $item = $menu ? $menu->getContainer()->findBy('uuid', $values['uuid']) : null; // if we can't find this menu or item, we assume someone // deleted it, and rather than resurrect it, we skip it. if (!$menu || !$item) { continue; } // handle deleted menu items. if ($values['remove']) { $item->getParent()->removePage($item); $menu->save(); continue; } // skip menu items that haven't changed. $form->populate($item->toArray()); if ($values['label'] == $form->getValue('label') && $values['position'] == $form->getValue('position') && $values['location'] == $form->getValue('location') && $values['contentAction'] == $form->getValue('contentAction') ) { continue; } } // skip aborted adds (user adds then clicks remove) if ($values['remove']) { continue; } $form->populate($values); // fetch the target menu so that save menu item will write to our copy $targetId = reset(explode('/', $form->getValue('location'), 2)); $target = $targetId ? $fetchMenu($targetId) : null; Menu_ManageController::saveMenuItem($form, $item, $menu, $target); } } ); // if an entry is deleted remove any content links (so long as they have no children) P4Cms_PubSub::subscribe('p4cms.content.record.delete', function(P4Cms_Content $entry) { foreach (P4Cms_Menu::fetchMixed(null, $entry->getAdapter()) as $mixed) { $item = $mixed->getMenuItem(); if ($item instanceof P4Cms_Navigation_Page_Content && $item->contentId == $entry->getId() && !count($item->getPages()) ) { $item->getParent()->removePage($item); $mixed->getMenu()->save(); } } } ); // organize menu records when pulling changes. P4Cms_PubSub::subscribe( 'p4cms.site.branch.pull.groupPaths', function($paths, $source, $target, $result) { $paths->addSubGroup( array( 'label' => 'Menus', 'basePaths' => $target->getId() . '/menus/...', 'inheritPaths' => $target->getId() . '/menus/...', 'pullByDefault' => true, 'details' => function($paths) use ($source, $target) { $pathsById = array(); foreach ($paths as $path) { if (strpos($path->depotFile, $target->getId() . '/menus/') === 0) { $pathsById[P4Cms_Menu::depotFileToId($path->depotFile)] = $path; } } $details = new P4Cms_Model_Iterator; $entries = Site_Model_PullPathGroup::fetchRecords( array_keys($pathsById), 'P4Cms_Menu', $source, $target ); foreach ($entries as $entry) { $path = $pathsById[$entry->getId()]; $details[] = new P4Cms_Model( array( 'conflict' => $path->conflict, 'action' => $path->action, 'label' => $entry->getLabel() ) ); } $details->setProperty( 'columns', array('label' => 'Menu', 'action' => 'Action') ); return $details; } ) ); } ); // automatically resolve menu conflicts. P4Cms_PubSub::subscribe( 'p4cms.site.branch.pull.conflicts', function($conflicts, $target, $source, $headChange, $preview, $adapter) { // skip the resolve when previewing so the user receives a warning // that there is the potential for changes to be lost in the target if ($preview) { return; } // filter conflicts for menu path entries and convert // depot paths to record ids so we can query records. $sourceIds = array(); $targetIds = array(); $basePath = $target->getId() . '/menus/'; foreach ($conflicts->getData() as $conflict) { if (isset($conflict['depotFile']) && strpos($conflict['depotFile'], $basePath) === 0 ) { $id = P4Cms_Menu::depotFileToId($conflict['depotFile'], $adapter); $sourceIds[] = $id . "@" . $headChange; $targetIds[] = $id; } } // if there are no menu conflicts, nothing to do. if (!$sourceIds || !$targetIds) { return; } // we use the given adapter for target rather than calling // target->getStorageAdapter() to ensure we are using the // proper user and workspace. $targetAdapter = $adapter; $sourceAdapter = $source->getStorageAdapter(); // fetch all conflicting menus in both the source and // target branches, so that we can merge them. $targetMenus = P4Cms_Menu::fetchAll(array('ids' => $targetIds), $targetAdapter); $sourceMenus = P4Cms_Menu::fetchAll(array('ids' => $sourceIds), $sourceAdapter); foreach ($targetMenus as $id => $targetMenu) { // if the source menu cannot be fetched we assume its deleted // and skip it as the default merge will take care of it. // deleted targets don't get fetched/looped automatically. if (!isset($sourceMenus[$id])) { continue; } // determine the base version to diff/merge against $resolve = $targetAdapter->getConnection()->run( 'resolve', array('-no', $targetMenu->toP4File()->getFilespec()) )->getData(0); $baseFile = P4_File::fetch($resolve['baseFile'] . '#' . $resolve['baseRev']); $baseMenu = P4Cms_Menu::fromP4File($baseFile, P4Cms_Menu::FROM_FILE_IMPORT); $sourceMenu = $sourceMenus[$id]; // merge in changes from target menu since last pull (base) // this will propagate non-conflicting changes made in // the target onto the source branch's menu container. $sourceMenu->merge($targetMenu, $baseMenu); // update the target with our merged result $targetMenu->setContainer($sourceMenu->getContainer()) ->save(); // run an 'accept yours' merge on the menu to avoid it being // clobbered by the source. we need to include the undoc -e // flag for the base to advance (which is key to our merge/diff) $targetAdapter->getConnection()->run( 'resolve', array('-e', '-ay', $targetMenu->toP4File()->getFilespec()) ); } } ); } }