diff --git a/core/lib/Drupal/Core/Ajax/AddCssCommand.php b/core/lib/Drupal/Core/Ajax/AddCssCommand.php new file mode 100644 index 0000000..c203a51 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AddCssCommand.php @@ -0,0 +1,53 @@ + tag. + */ + public function __construct($styles) { + $this->styles = $styles; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'add_css', + 'data' => $this->styles, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/AfterCommand.php b/core/lib/Drupal/Core/Ajax/AfterCommand.php new file mode 100644 index 0000000..5c3e6f0 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AfterCommand.php @@ -0,0 +1,40 @@ + 'insert', + 'method' => 'after', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php new file mode 100644 index 0000000..4932dab --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -0,0 +1,127 @@ +commands[] = $command->render(); + return $this; + } + + /** + * Sets the response's data to be the array of AJAX commands. + * + * @param + * $request A request object. + * + * @return + * Response The current response. + */ + public function prepare(Request $request) { + + parent::setData($this->ajaxRender()); + return parent::prepare($request); + } + + /** + * Prepares the AJAX commands for sending back to the client. + * + * @return array + * An array of commands ready to be returned as JSON. + */ + protected function ajaxRender() { + // Ajax responses aren't rendered with html.tpl.php, so we have to call + // drupal_get_css() and drupal_get_js() here, in order to have new files + // added during this request to be loaded by the page. We only want to send + // back files that the page hasn't already loaded, so we implement simple + // diffing logic using array_diff_key(). + foreach (array('css', 'js') as $type) { + // It is highly suspicious if $_POST['ajax_page_state'][$type] is empty, + // since the base page ought to have at least one JS file and one CSS file + // loaded. It probably indicates an error, and rather than making the page + // reload all of the files, instead we return no new files. + if (empty($_POST['ajax_page_state'][$type])) { + $items[$type] = array(); + } else { + $function = 'drupal_add_' . $type; + $items[$type] = $function(); + drupal_alter($type, $items[$type]); + // @todo Inline CSS and JS items are indexed numerically. These can't be + // reliably diffed with array_diff_key(), since the number can change + // due to factors unrelated to the inline content, so for now, we + // strip the inline items from Ajax responses, and can add support for + // them when drupal_add_css() and drupal_add_js() are changed to using + // md5() or some other hash of the inline content. + foreach ($items[$type] as $key => $item) { + if (is_numeric($key)) { + unset($items[$type][$key]); + } + } + // Ensure that the page doesn't reload what it already has. + $items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]); + } + } + + // Render the HTML to load these files, and add AJAX commands to insert this + // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the + // data from being altered again, as we already altered it above. Settings + // are handled separately, afterwards. + if (isset($items['js']['settings'])) { + unset($items['js']['settings']); + } + $styles = drupal_get_css($items['css'], TRUE); + $scripts_footer = drupal_get_js('footer', $items['js'], TRUE); + $scripts_header = drupal_get_js('header', $items['js'], TRUE); + + if (!empty($styles)) { + $this->addCommand(new AddCssCommand($styles)); + } + if (!empty($scripts_header)) { + $this->addCommand(new PrependCommand('head', $scripts_header)); + } + if (!empty($scripts_footer)) { + $this->addCommand(new AppendCommand('body', $scripts_footer)); + } + + // Now add a command to merge changes and additions to Drupal.settings. + $scripts = drupal_add_js(); + if (!empty($scripts['settings'])) { + $settings = $scripts['settings']; + $this->addCommand(new SettingsCommand(call_user_func_array('array_merge_recursive', $settings['data']), TRUE)); + } + + $commands = $this->commands; + drupal_alter('ajax_render', $commands); + + return $commands; + } + +} diff --git a/core/lib/Drupal/Core/Ajax/AlertCommand.php b/core/lib/Drupal/Core/Ajax/AlertCommand.php new file mode 100644 index 0000000..db935ab --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AlertCommand.php @@ -0,0 +1,45 @@ +text = $text; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'alert', + 'text' => $this->text, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/AppendCommand.php b/core/lib/Drupal/Core/Ajax/AppendCommand.php new file mode 100644 index 0000000..067af41 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AppendCommand.php @@ -0,0 +1,40 @@ + 'insert', + 'method' => 'append', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/BeforeCommand.php b/core/lib/Drupal/Core/Ajax/BeforeCommand.php new file mode 100644 index 0000000..9b750a6 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/BeforeCommand.php @@ -0,0 +1,40 @@ + 'insert', + 'method' => 'before', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/ChangedCommand.php b/core/lib/Drupal/Core/Ajax/ChangedCommand.php new file mode 100644 index 0000000..49becb3 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/ChangedCommand.php @@ -0,0 +1,65 @@ +selector = $selector; + $this->asterisk = $asterisk; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'changed', + 'selector' => $this->selector, + 'asterisk' => $this->asterisk, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/CommandInterface.php b/core/lib/Drupal/Core/Ajax/CommandInterface.php new file mode 100644 index 0000000..51d197a --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/CommandInterface.php @@ -0,0 +1,22 @@ +selector = $selector; + $this->css = $css; + } + + /** + * Adds a property/value pair to the CSS to be added to this element. + * + * @param $property + * The CSS property to be changed. + * @param $value + * The new value of the CSS property. + */ + public function setProperty($property, $value) { + $this->css[$property] = $value; + return $this; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'css', + 'selector' => $this->selector, + 'argument' => $this->css, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/DataCommand.php b/core/lib/Drupal/Core/Ajax/DataCommand.php new file mode 100644 index 0000000..676f30c --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/DataCommand.php @@ -0,0 +1,79 @@ +selector = $selector; + $this->name = $name; + $this->value = $value; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'data', + 'selector' => $this->selector, + 'name' => $this->name, + 'value' => $this->value, + ); + } + +} + diff --git a/core/lib/Drupal/Core/Ajax/HtmlCommand.php b/core/lib/Drupal/Core/Ajax/HtmlCommand.php new file mode 100644 index 0000000..16841e2 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/HtmlCommand.php @@ -0,0 +1,40 @@ + 'insert', + 'method' => 'html', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/InsertCommand.php b/core/lib/Drupal/Core/Ajax/InsertCommand.php new file mode 100644 index 0000000..bee3a38 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/InsertCommand.php @@ -0,0 +1,78 @@ +selector = $selector; + $this->html = $html; + $this->settings = $settings; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'insert', + 'method' => NULL, + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/InvokeCommand.php b/core/lib/Drupal/Core/Ajax/InvokeCommand.php new file mode 100644 index 0000000..12589af --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/InvokeCommand.php @@ -0,0 +1,78 @@ +selector = $selector; + $this->method = $method; + $this->arguments = $arguments; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'invoke', + 'selector' => $this->selector, + 'method' => $this->method, + 'args' => $this->arguments, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/PrependCommand.php b/core/lib/Drupal/Core/Ajax/PrependCommand.php new file mode 100644 index 0000000..7cdb376 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/PrependCommand.php @@ -0,0 +1,40 @@ + 'insert', + 'method' => 'prepend', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/RemoveCommand.php b/core/lib/Drupal/Core/Ajax/RemoveCommand.php new file mode 100644 index 0000000..078dd32 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/RemoveCommand.php @@ -0,0 +1,53 @@ +selector = $selector; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + return array( + 'command' => 'remove', + 'selector' => $this->selector, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/ReplaceCommand.php b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php new file mode 100644 index 0000000..94475b1 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php @@ -0,0 +1,40 @@ + 'insert', + 'method' => 'replaceWith', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/RestripeCommand.php b/core/lib/Drupal/Core/Ajax/RestripeCommand.php new file mode 100644 index 0000000..4fe7d80 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/RestripeCommand.php @@ -0,0 +1,54 @@ +selector = $selector; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'restripe', + 'selector' => $this->selector, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/SettingsCommand.php b/core/lib/Drupal/Core/Ajax/SettingsCommand.php new file mode 100644 index 0000000..ee331e2 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/SettingsCommand.php @@ -0,0 +1,69 @@ +settings = $settings; + $this->merge = $merge; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'settings', + 'settings' => $this->settings, + 'merge' => $this->merge, + ); + } + +} diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 6694598..445e7ab 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -10,6 +10,8 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Drupal\file\FileUsage\DatabaseFileUsageBackend; use Drupal\file\FileUsage\FileUsageInterface; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; // Load all Field module hooks for File. require_once DRUPAL_ROOT . '/core/modules/file/file.field.inc'; @@ -750,9 +752,8 @@ function file_ajax_upload() { if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) { // Invalid request. drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error'); - $commands = array(); - $commands[] = ajax_command_replace(NULL, theme('status_messages')); - return array('#type' => 'ajax', '#commands' => $commands); + $response = new AjaxResponse(); + return $response->addCommand(new ReplaceCommand(NULL, theme('status_messages'))); } list($form, $form_state) = ajax_get_form(); @@ -760,9 +761,8 @@ function file_ajax_upload() { if (!$form) { // Invalid form_build_id. drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error'); - $commands = array(); - $commands[] = ajax_command_replace(NULL, theme('status_messages')); - return array('#type' => 'ajax', '#commands' => $commands); + $response = new AjaxResponse(); + return $response->addCommand(new ReplaceCommand(NULL, theme('status_messages'))); } // Get the current element and count the number of files. @@ -793,9 +793,8 @@ function file_ajax_upload() { $js = drupal_add_js(); $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); - $commands = array(); - $commands[] = ajax_command_replace(NULL, $output, $settings); - return array('#type' => 'ajax', '#commands' => $commands); + $response = new AjaxResponse(); + return $response->addCommand(new ReplaceCommand(NULL, $output, $settings)); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php new file mode 100644 index 0000000..2e6c421 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php @@ -0,0 +1,309 @@ + 'Ajax Command Objects', + 'description' => 'Test that each AJAX command object can be created and rendered', + 'group' => 'AJAX', + ); + } + + /** + * Tests that AddCssCommand objects can be constructed and rendered. + */ + function testAddCssCommand() { + + $command = new AddCssCommand('p{ text-decoration:blink; }'); + + $expected = array( + 'command' => 'add_css', + 'data' => 'p{ text-decoration:blink; }', + ); + + $this->assertEqual($command->render(), $expected, 'AddCssCommand::render() returns a proper array.'); + } + + /** + * Tests that AfterCommand objecst can be constructed and rendered. + */ + function testAfterCommand() { + + $command = new AfterCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => 'after', + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'AfterCommand::render() returns a proper array.'); + } + + /** + * Tests that AlertCommand objects can be constructed and rendered. + */ + function testAlertCommand() { + $command = new AlertCommand('Set condition 1 throughout the ship!'); + $expected = array( + 'command' => 'alert', + 'text' => 'Set condition 1 throughout the ship!', + ); + + $this->assertEqual($command->render(), $expected, 'AlertCommand::render() returns a proper array.'); + } + + /** + * Tests that AppendCommand objects can be constructed and rendered. + */ + function testAppendCommand() { + // Test AppendCommand. + $command = new AppendCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => 'append', + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'AppendCommand::render() returns a proper array.'); + } + + /** + * Tests that BeforeCommand objects can be constructed and rendered. + */ + function testBeforeCommand() { + + $command = new BeforeCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => 'before', + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'BeforeCommand::render() returns a proper array.'); + } + + /** + * Tests that ChangedCommand objects can be constructed and rendered. + */ + function testChangedCommand() { + $command = new ChangedCommand('#page-title', '#page-title-changed'); + + $expected = array( + 'command' => 'changed', + 'selector' => '#page-title', + 'asterisk' => '#page-title-changed', + ); + + $this->assertEqual($command->render(), $expected, 'ChangedCommand::render() returns a proper array.'); + } + + /** + * Tests that CssCommand objects can be constructed and rendered. + */ + function testCssCommand() { + + $command = new CssCommand('#page-title', array('text-decoration' => 'blink')); + $command->setProperty('font-size', '40px')->setProperty('font-weight', 'bold'); + + $expected = array( + 'command' => 'css', + 'selector' => '#page-title', + 'argument' => array( + 'text-decoration' => 'blink', + 'font-size' => '40px', + 'font-weight' => 'bold', + ), + ); + + $this->assertEqual($command->render(), $expected, 'CssCommand::render() returns a proper array.'); + } + + /** + * Tests that DataCommand objects can be constructed and rendered. + */ + function testDataCommand() { + + $command = new DataCommand('#page-title', 'my-data', array('key' => 'value')); + + $expected = array( + 'command' => 'data', + 'selector' => '#page-title', + 'name' => 'my-data', + 'value' => array('key' => 'value'), + ); + + $this->assertEqual($command->render(), $expected, 'DataCommand::render() returns a proper array.'); + } + + /** + * Tests that HtmlCommand objects can be constructed and rendered. + */ + function testHtmlCommand() { + + $command = new HtmlCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => 'html', + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'HtmlCommand::render() returns a proper array.'); + } + + /** + * Tests that InsertCommand objects can be constructed and rendered. + */ + function testInsertCommand() { + + $command = new InsertCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => NULL, + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'InsertCommand::render() returns a proper array.'); + } + + /** + * Tests that InvokeCommand objects can be constructed and rendered. + */ + function testInvokeCommand() { + + $command = new InvokeCommand('#page-title', 'myMethod', array('var1', 'var2')); + + $expected = array( + 'command' => 'invoke', + 'selector' => '#page-title', + 'method' => 'myMethod', + 'args' => array('var1', 'var2'), + ); + + $this->assertEqual($command->render(), $expected, 'InvokeCommand::render() returns a proper array.'); + } + + /** + * Tests that PrependCommand objects can be constructed and rendered. + */ + function testPrependCommand() { + + $command = new PrependCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => 'prepend', + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'PrependCommand::render() returns a proper array.'); + } + + /** + * Tests that RemoveCommand objects can be constructed and rendered. + */ + function testRemoveCommand() { + + $command = new RemoveCommand('#page-title'); + + $expected = array( + 'command' => 'remove', + 'selector' => '#page-title', + ); + + $this->assertEqual($command->render(), $expected, 'RemoveCommand::render() returns a proper array.'); + } + + /** + * Tests that ReplaceCommand objects can be constructed and rendered. + */ + function testReplaceCommand() { + $command = new ReplaceCommand('#page-title', '

New Text!

', array('my-setting' => 'setting')); + + $expected = array( + 'command' => 'insert', + 'method' => 'replaceWith', + 'selector' => '#page-title', + 'data' => '

New Text!

', + 'settings' => array('my-setting' => 'setting'), + ); + + $this->assertEqual($command->render(), $expected, 'ReplaceCommand::render() returns a proper array.'); + } + + /** + * Tests that RestripeCommand objects can be constructed and rendered. + */ + function testRestripeCommand() { + $command = new RestripeCommand('#page-title'); + + $expected = array( + 'command' => 'restripe', + 'selector' => '#page-title', + ); + + $this->assertEqual($command->render(), $expected, 'RestripeCommand::render() returns a proper array.'); + } + + /** + * Tests that SettingsCommand objects can be constructed and rendered. + */ + function testSettingsCommand() { + $command = new SettingsCommand(array('key' => 'value'), TRUE); + + $expected = array( + 'command' => 'settings', + 'settings' => array('key' => 'value'), + 'merge' => TRUE, + ); + + $this->assertEqual($command->render(), $expected, 'SettingsCommand::render() returns a proper array.'); + } + +} +