diff --git a/core/lib/Drupal/Core/Ajax/AddCssCommand.php b/core/lib/Drupal/Core/Ajax/AddCssCommand.php new file mode 100644 index 0000000..00a3799 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AddCssCommand.php @@ -0,0 +1,46 @@ + tag. + * + * @var string + */ + protected $styles; + + public function __construct($styles) { + $this->styles = $styles; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'add_css', + 'data' => $this->styles, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/AfterCommand.php b/core/lib/Drupal/Core/Ajax/AfterCommand.php new file mode 100644 index 0000000..18d8d26 --- /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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php new file mode 100644 index 0000000..d5a5a77 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -0,0 +1,120 @@ +commands[] = $command->render(); + } + + /** + * 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; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/AlertCommand.php b/core/lib/Drupal/Core/Ajax/AlertCommand.php new file mode 100644 index 0000000..d112682 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AlertCommand.php @@ -0,0 +1,39 @@ +text = $text; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'alert', + 'text' => $this->text, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/AppendCommand.php b/core/lib/Drupal/Core/Ajax/AppendCommand.php new file mode 100644 index 0000000..5623827 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AppendCommand.php @@ -0,0 +1,38 @@ + 'insert', + 'method' => 'append', + 'selector' => $this->selector, + 'data' => $this->html, + 'settings' => $this->settings, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/BeforeCommand.php b/core/lib/Drupal/Core/Ajax/BeforeCommand.php new file mode 100644 index 0000000..a998c8f --- /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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/ChangedCommand.php b/core/lib/Drupal/Core/Ajax/ChangedCommand.php new file mode 100644 index 0000000..f21e6b9 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/ChangedCommand.php @@ -0,0 +1,56 @@ +selector = $selector; + $this->asterisk = $asterisk; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'changed', + 'selector' => $this->selector, + 'asterisk' => $this->asterisk, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/CommandInterface.php b/core/lib/Drupal/Core/Ajax/CommandInterface.php new file mode 100644 index 0000000..7b15e37 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/CommandInterface.php @@ -0,0 +1,23 @@ +selector = $selector; + $this->css = $css; + } + + /** + * Adds a key/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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/DataCommand.php b/core/lib/Drupal/Core/Ajax/DataCommand.php new file mode 100644 index 0000000..553c97f --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/DataCommand.php @@ -0,0 +1,69 @@ +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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/HtmlCommand.php b/core/lib/Drupal/Core/Ajax/HtmlCommand.php new file mode 100644 index 0000000..008cf72 --- /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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/InsertCommand.php b/core/lib/Drupal/Core/Ajax/InsertCommand.php new file mode 100644 index 0000000..ae96b70 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/InsertCommand.php @@ -0,0 +1,67 @@ +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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/InvokeCommand.php b/core/lib/Drupal/Core/Ajax/InvokeCommand.php new file mode 100644 index 0000000..ca3fb98 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/InvokeCommand.php @@ -0,0 +1,66 @@ +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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/PrependCommand.php b/core/lib/Drupal/Core/Ajax/PrependCommand.php new file mode 100644 index 0000000..76dcd66 --- /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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/RemoveCommand.php b/core/lib/Drupal/Core/Ajax/RemoveCommand.php new file mode 100644 index 0000000..b4d755a --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/RemoveCommand.php @@ -0,0 +1,44 @@ +selector = $selector; + } + + public function render() { + return array( + 'command' => 'remove', + 'selector' => $this->selector, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/ReplaceCommand.php b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php new file mode 100644 index 0000000..9f05d30 --- /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, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/RestripeCommand.php b/core/lib/Drupal/Core/Ajax/RestripeCommand.php new file mode 100644 index 0000000..0db8222 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/RestripeCommand.php @@ -0,0 +1,46 @@ +selector = $selector; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'restripe', + 'selector' => $this->selector, + ); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Ajax/SettingsCommand.php b/core/lib/Drupal/Core/Ajax/SettingsCommand.php new file mode 100644 index 0000000..b5fbf03 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/SettingsCommand.php @@ -0,0 +1,60 @@ +settings = $settings; + $this->merge = $merge; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + + return array( + 'command' => 'settings', + 'settings' => $this->settings, + 'merge' => $this->merge, + ); + } + +} \ No newline at end of file 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..11e8be0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php @@ -0,0 +1,238 @@ + 'Ajax Command Objects', + 'description' => 'Test that each AJAX command object can be created and rendered', + 'group' => 'AJAX', + ); + } + + /** + * Tests that each AJAX command class can be constructed and rendered. + */ + function testAjaxCommands() { + + // Test AddCssCommand. + $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.'); + + // Test AfterCommand. + $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.'); + + // Test AlertCommand. + $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.'); + + // 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.'); + + // Test BeforeCommand. + $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.'); + + // Test ChangedCommand. + $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.'); + + // Test CssCommand. + $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.'); + + // Test DataCommand. + $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.'); + + // Test HtmlCommand. + $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.'); + + // Test InsertCommand. + $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.'); + + // Test InvokeCommand. + $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.'); + + // Test PrependCommand. + $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.'); + + // Test RemoveCommand. + $command = new RemoveCommand('#page-title'); + + $expected = array( + 'command' => 'remove', + 'selector' => '#page-title', + ); + + $this->assertEqual($command->render(), $expected, 'RemoveCommand::render() returns a proper array.'); + + // Test ReplaceCommand. + $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.'); + + // Test RestripeCommand. + $command = new RestripeCommand('#page-title'); + + $expected = array( + 'command' => 'restripe', + 'selector' => '#page-title', + ); + + $this->assertEqual($command->render(), $expected, 'RestripeCommand::render() returns a proper array.'); + + // Test SettingsCommand. + $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.'); + } +}