Phantom JS Selenium IDE : How to resize the browser window - node.js

I'm using Selenium IDE to record a simple test with my web app , my html generated scènario is like the following the following:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="selenium.base" href="http://pagamac.dev/" />
<title>addContact</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">addContact</td></tr>
</thead><tbody>
<tr>
<td>open</td>
<td>/</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=edit-name</td>
<td>admin</td>
</tr>
<tr>
<td>click</td>
<td>id=edit-pass</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=edit-pass</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=edit-submit</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>css=li.civicrm > a</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>//*[#id="civicrm-menu"]/li[4]</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>//*[#id="root-menu-div"]/div[5]/ul/li[1]/div/a</td>
<td>5000</td>
</tr>
<tr>
<td>select</td>
<td>//*[#id="prefix_id"]</td>
<td>M.</td>
</tr>
<tr>
<td>click</td>
<td>id=first_name</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=first_name</td>
<td>franc</td>
</tr>
<tr>
<td>click</td>
<td>id=last_name</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=last_name</td>
<td>ribery</td>
</tr>
<tr>
<td>select</td>
<td>//*[#id="email_1_location_type_id"]</td>
<td>Autre</td>
</tr>
<tr>
<td>click</td>
<td>id=email_1_email</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=email_1_email</td>
<td>rib#gmail.com</td>
</tr>
<tr>
<td>click</td>
<td>//*[#id="Email_1_IsBulkmail"]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>//*[#id="email[1][on_hold]"]</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=phone_1_phone</td>
<td>8888888888</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>id=_qf_Contact_upload_view-bottom</td>
<td></td>
</tr>
</tbody></table>
</body>
</html>
I'm using a specific plugin : selenium-html-js-converter (you find it here:
https://www.npmjs.com/package/selenium-html-js-converter )
this plugin converts my html test to a js one , like the following :
"use strict";
/* jslint node: true */
var assert = require('assert');
var browser, element, currentCommand = '',
options = {
timeout: 30000,
retries: 0,
screenshotFolder: 'screenshots/test_add_SimpleContact',
baseUrl: 'http://pagamac.dev/'
};
module.exports = function testAddSimpleContact(_browser, _options) {
browser = _browser;
var acceptNextAlert = true;
getRuntimeOptions(_options);
try {
currentCommand = 'open("/", "")';
browser.get(addBaseUrl("/"));
currentCommand = 'type("id=edit-name", "admin")';
browser.elementById("edit-name").clear();
browser.elementById("edit-name").sendKeys("admin");
currentCommand = 'click("id=edit-pass", "")';
browser.elementById("edit-pass").click();
currentCommand = 'type("id=edit-pass", "admin")';
browser.elementById("edit-pass").clear();
browser.elementById("edit-pass").sendKeys("admin");
currentCommand = 'clickAndWait("id=edit-submit", "")';
doAndWait(function() {
browser.elementById("edit-submit").click();
});
currentCommand = 'clickAndWait("css=li.civicrm > a", "")';
doAndWait(function() {
browser.elementByCssSelector("li.civicrm > a").click();
});
currentCommand = 'click("//*[#id="civicrm-menu"]/li[4]", "")';
browser.elementByXPath("//*[#id=\"civicrm-menu\"]/li[4]").click();
currentCommand = 'clickAndWait("//*[#id="root-menu-div"]/div[5]/ul/li[1]/div/a", "5000")';
doAndWait(function() {
browser.elementByXPath("//*[#id=\"root-menu-div\"]/div[5]/ul/li[1]/div/a").click();
});
currentCommand = 'select("//*[#id="prefix_id"]", "M.")';
browser.elementByXPath("//*[#id=\"prefix_id\"]").elementByXPath('option[text()="M."][1]').click();
currentCommand = 'click("id=first_name", "")';
browser.elementById("first_name").click();
currentCommand = 'type("id=first_name", "franc")';
browser.elementById("first_name").clear();
browser.elementById("first_name").sendKeys("franc");
currentCommand = 'click("id=last_name", "")';
browser.elementById("last_name").click();
currentCommand = 'type("id=last_name", "ribery")';
browser.elementById("last_name").clear();
browser.elementById("last_name").sendKeys("ribery");
currentCommand = 'select("//*[#id="email_1_location_type_id"]", "Autre")';
browser.elementByXPath("//*[#id=\"email_1_location_type_id\"]").elementByXPath('option[text()="Autre"][1]').click();
currentCommand = 'click("id=email_1_email", "")';
browser.elementById("email_1_email").click();
currentCommand = 'type("id=email_1_email", "rib#gmail.com")';
browser.elementById("email_1_email").clear();
browser.elementById("email_1_email").sendKeys("rib#gmail.com");
currentCommand = 'click("//*[#id="Email_1_IsBulkmail"]", "")';
browser.elementByXPath("//*[#id=\"Email_1_IsBulkmail\"]").click();
currentCommand = 'click("//*[#id="email[1][on_hold]"]", "")';
browser.elementByXPath("//*[#id=\"email[1][on_hold]\"]").click();
currentCommand = 'type("id=phone_1_phone", "8888888888")';
browser.elementById("phone_1_phone").clear();
browser.elementById("phone_1_phone").sendKeys("8888888888");
currentCommand = 'clickAndWait("id=_qf_Contact_upload_view-bottom", "")';
doAndWait(function() {
browser.elementById("_qf_Contact_upload_view-bottom").click();
});
} catch (e) {
var failedScreenShot = options.screenshotFolder + '/Exception#' + currentCommand.replace(/\(.+/, '') + '.png';
try {
createFolderPath(options.screenshotFolder);
browser.saveScreenshot(failedScreenShot);
} catch (e) {
e.message = 'Failure in Selenium command "' + currentCommand + '": ' + e.message + ' (Could not save screenshot after failure occured)';
throw e;
}
e.message = 'Failure in Selenium command "' + currentCommand + '": ' + e.message + ' (Screenshot was saved to ' + failedScreenShot + ')';
throw e;
}
};
/**
* A hacky way to implement *AndWait Selenese commands. Native wd doesn't offer
* commands equivalent to e.g. clickAndWait or dragAndDropAndWait (actually
* neither dragAndDrop nor AndWait exist in wd) or clickAndWait. We work around
* it wrapping all *AndWait commands in code that first taints the document body
* with a class, then runs the base command, and waits for a new document ready
* state without the tainted body.
*
* #param {function} code The code to execute
* #param {WdSyncClient.browser} wdBrowser (optional) Browser instance
* #return {void}
*/
function doAndWait(code, wdBrowser) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
wdBrowser.execute('document.body.className += " SHTML2JSC"');
code();
withRetry(function() {
if (wdBrowser.execute("return document.readyState") !== 'complete' || wdBrowser.hasElementByCssSelector('body.SHTML2JSC'))
throw new Error('Page did not load in time');
}, wdBrowser);
}
/**
* Implements waitForPageToLoad selenese command. As opposed to the Selenium
* IDE implementation, this one actually waits for all resources to have been
* loaded.
*
* #param {WdSyncClient.browser} wdBrowser (optional) Browser instance.
* #return {void}
*/
function waitForPageToLoad(wdBrowser) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
withRetry(function() {
if (wdBrowser.execute("return document.readyState") !== 'complete')
throw new Error('Page did not load in time');
});
}
function getRuntimeOptions(opts) {
if (!opts) return;
if (typeof opts.lbParam === 'object') {
options.lbParam = opts.lbParam;
}
if (opts.baseUrl && typeof opts.baseUrl === 'string') {
options.baseUrl = opts.baseUrl;
if (opts.forceBaseUrl && typeof opts.forceBaseUrl === 'boolean') {
options.forceBaseUrl = opts.forceBaseUrl;
}
}
if (opts.screenshotFolder && typeof opts.screenshotFolder === 'string') {
options.screenshotFolder = opts.screenshotFolder;
}
if (opts.timeout && isNumber(opts.timeout)) {
options.timeout = opts.timeout;
}
if (opts.retries && isNumber(opts.retries)) {
options.retries = opts.retries;
}
}
function isNumber(val) {
return typeof val === 'number' && !isNaN(val);
}
function isAlertPresent(wdBrowser) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
try {
wdBrowser.alertText();
return true;
} catch (e) {
return false;
}
}
function closeAlertAndGetItsText(acceptNextAlert, wdBrowser) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
try {
var alertText = wdBrowser.alertText();
if (acceptNextAlert) {
wdBrowser.acceptAlert();
} else {
wdBrowser.dismissAlert();
}
return alertText;
} catch (ignore) {}
}
function isEmptyArray(arr) {
return arr instanceof Array && arr.length === 0;
}
function waitFor(checkFunc, expression, timeout, pollFreq, wdBrowser) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
if (!isNumber(timeout)) {
timeout = options.timeout;
}
if (!isNumber(pollFreq)) {
pollFreq = 200;
}
var val;
var timeLeft = timeout;
while (!val) {
val = checkFunc();
if (val)
break;
if (timeLeft < 0) {
throw new Error('Timed out after ' + timeout + ' msecs waiting for expression: ' + expression);
}
wdBrowser.sleep(pollFreq);
timeLeft -= pollFreq;
}
return val;
}
function createFolderPath(path) {
var fs = require('fs');
var folders = path.split(/[/\\]+/);
path = '';
while (folders.length) {
/* This works for both absolute and relative paths, as split on an absolute path will have resulted in an array with the first bit empty. Safe for absolute Windows paths as well: */
path += folders.shift() + '/';
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
} else if (!fs.statSync(path).isDirectory()) {
throw new Error("Cannot create directory '" + path + "'. File of same name already exists.");
}
}
}
/**
* Prefix a (relative) path with a base url.
*
* If the path itself is an absolute one including a domain, it'll be returned as-is, unless force is set to true, in
* which case the existing domain is replaced with the base.
*
* When optional arguments are when omitted, values from glocal options object are used.
*
* #param {string} path The path to prefix with the base url
* #param {string} base (optional) The base url
* #param {bool} force (optional) If true, force prefixing even if path is an absolute url
* #return {string} The prefixed url
*/
function addBaseUrl(path, base, force) {
if (typeof base !== 'string') {
base = options.baseUrl;
}
if (typeof force !== 'boolean') {
force = options.forceBaseUrl;
}
if (path.match(/^http/)) {
if (force) {
return path.replace(/^http(s?):\/\/[^/]+/, base).replace(/([^:])\/\/+/g, '$1/');
}
return path;
}
return (base + '/' + path).replace(/([^:])\/\/+/g, '$1/');
}
/**
* Focuses the topmost window on the stack of handles in the browser.
*
* After a WdSyncClient.browser.close() wd does not automatically restore focus
* to the previous window on the stack, so you may execute this function to
* ensure that subsequent tests won't be targeting a defunct window handle.
*
* #param {WdSyncClient.browser} wdBrowser (optional) Browser instance.
* #return {void}
*/
function refocusWindow(wdBrowser) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
var handles = wdBrowser.windowHandles();
if (handles.length) {
try {
wdBrowser.window(handles[handles.length - 1]);
} catch (e) {
console.warn('Failed to automatically restore focus to topmost window on browser stack. Error:', e);
}
}
}
/**
* Tries to execute an Error throwing function, and if an error is thrown, one
* or more retries are attempted until <timeout> msecs have passed.
*
* Pauses between retries are increasing in length. The pause before the final
* retry will be half the total timeout. The pause before the second-to-last
* will be half of the last one's, and so forth. The first attempt will have the
* same pause as that of the first retry.
*
* Optional arguments use glocal values when omitted
*
* #param {function} code The code to execute
* #param {WdSyncClient.browser} wdBrowser (optional) Browser instance
* #param {number} retries (optional) The max number of retries
* #param {number} timeout (optional) The max number of msecs to keep trying
* #return {mixed} Whatever the code block returns
*/
function withRetry(code, wdBrowser, retries, timeout) {
if (typeof wdBrowser !== 'object') {
wdBrowser = browser;
}
if (!isNumber(retries)) {
retries = options.retries;
}
if (!isNumber(timeout)) {
timeout = options.timeout;
}
var durations = [timeout];
var err;
while (retries) {
durations[0] = Math.ceil(durations[0] / 2);
durations.unshift(durations[0]);
--retries;
}
for (var i = 0; i < durations.length; ++i) {
try {
return code();
} catch (e) {
err = e;
wdBrowser.sleep(durations[i]);
}
}
throw (err);
}
/**
* Triggers a keyboard event on the provided wd browser element.
*
* #param {WD Element} element Target DOM element to trigger the event on
* #param {string} event Keyboard event (keyup|keydown|keypress)
* #param {keyCode} key Charcode to use
* #return {void}
*/
function keyEvent(element, event, keyCode) {
browser.execute(functionBody(function() {
var element = arguments[0];
var event = arguments[1];
var keyCode = arguments[2];
var ev = window.document.createEvent('KeyboardEvent');
if (ev.initKeyEvent)
ev.initKeyEvent(event, true, true, window, 0, 0, 0, 0, 0, keyCode);
else
ev.initKeyboardEvent(event, true, true, window, 0, 0, 0, 0, 0, keyCode);
return element.dispatchEvent(ev);
}), [element.rawElement, event, keyCode]);
}
function functionBody(func) {
return func.toString().replace(/^function[^{]+{/, '').replace(/}[^}]*$/, '');
}
/* User extensions */
function typeRedactor(target, value, element) {
/* .execute takes just a function body as a string to be eval'ed: */
var className = browser.execute(functionBody(function() {
var element = arguments[0];
var text = arguments[1];
var callback = arguments[2];
/* Once done, we tag redactor with a class, so we know when we finished: */
var className = "seleniumDoTypeRedactor-" + (new Date()).getTime();
var keyEvent = function(element, event, keyCode) {
var ev = window.document.createEvent('KeyboardEvent');
if (ev.initKeyEvent)
ev.initKeyEvent(event, true, true, window, 0, 0, 0, 0, 0, keyCode);
else
ev.initKeyboardEvent(event, true, true, window, 0, 0, 0, 0, 0, keyCode);
return element.dispatchEvent(ev);
};
keyEvent(element, 'keydown', 0);
keyEvent(element, 'keyup', 0);
element.textContent = 'redactor';
setTimeout(function() {
keyEvent(element, 'keydown', 0);
keyEvent(element, 'keyup', 0);
element.textContent = text;
setTimeout(function() {
keyEvent(element, 'keydown', 0);
keyEvent(element, 'keyup', 0);
element.className += ' ' + className;
}, 50);
}, 50);
return className;
}), [element.rawElement /* Important! element is mangled by wd-sync; we need the raw wd element */ , value]);
waitFor(function() {
return browser.hasElementByCssSelector('.' + className);
}, 'browser.hasElementByCssSelector(".' + className + '") [to mark completion of typeRedactor execution]');
}
finally to run this test i'm using node & phantom.js : first i run phantom.js ; then i execute my test.
My problem is when i do this ; my test , whitch runs well with within selenium IDE (in the html format) wouldn't work fine within phantom.js
After some debugging, i see that phatom.js is running under a wrong window sizes (weak with and height) -> By the way my web page is loosing some elements when resized to smaller.
I'm looking to how control the phantom.js browser windows sizes : width & heignt
Note : i'm note using the classic webdriver (you can notice it is my js test file)
suggestions ?

Faced the similar situation a couple of days ago. Browsed through a lot of discussion forums/discussions. The one suggestion I got regarding window resizing is this, you have to
beforeEach(function() {
browser.driver.manage().window().setSize(1280, 1024);
});
Having said that, this solution didn't serve my purpose. I had to perform some tricky measures.
Let me know if it works else I will suggest you more options.

Related

LWC Pagination with page number

I'm a junior developer, I need some help to implement an lwc component which display records in experience site so I need to implement pagination since we have a large number to record.
I implement the 2 buttons "Previous" and "Next" it works fine, but I need the a clickable number o page between the 2 buttons here is the mockup : something like :<- 1 2 3 4 ... ->
here is my code:
<template>
<div class="pastcfps"><br>
<table>
<thead>
<tr>
<th class="pastcfpstitle"></th>
<th class="fullProposalDeadlineTitle"><a data-id="Proposal_Submission_Deadline__c"
onclick={sort}>Closing Date<img src={sortArrowUp} data-id="sortIconFp"></a></th>
</tr>
</thead>
<tbody>
<template if:true={pastcfps}>
<template for:each={pastcfps} for:item="pastcfp">
<tr class="pastcfpstr" key={pastcfp.Id}>
<td key={pastcfp.Id} class="pastcfpstitle">
<p class="pastcfpsRecordType">{pastcfp.RecordType.Name}</p>
<a class="pastcfpsname" data-id={pastcfp.Id} onclick={redirect} is-loading={tableLoadingState}>{pastcfp.Name}</a>
</td>
<td key={pastcfp.Id} class="fullProposalDeadline">{pastcfp.Proposal_Submission_Deadline__c}
</td>
</tr>
</template>
</template>
</tbody>
<template if:true={error}>
<p class="pastcfpsname">There are no past Calls for Proposals</p>
</template>
</table>
</div>
<div style="text-align:center;">
<c-pagination-navigation onprevious={handlePrev} onnext={handleNext}></c-pagination-navigation>
</div>
</template>
import { LightningElement, api, wire, track } from 'lwc';
import getPastCfPs from '#salesforce/apex/CallsForProposalsService.getPastCfPs';
import ARROW_LOGO from '#salesforce/contentAssetUrl/homearrow';
import SORT_ARROW_UP from '#salesforce/resourceUrl/sortArrowUp';
import SORT_ARROW_DOWN from '#salesforce/resourceUrl/sortArrowDown';
export default class RecentOpenCallsForProposals extends LightningElement {
arrowLogo = ARROW_LOGO;
sortArrowUp = SORT_ARROW_UP;
sortArrowDown = SORT_ARROW_DOWN;
sortedDirection = 'asc';
sortedColumn;
#track tableLoadingState = false;
#track pastcfps;
#track error
#track offset=0;
#track Prevoffset=0;
limit = 12;
#wire(getPastCfPs,{ offset: '$offset', l : '$limit' }) wiredCfps({ data, error }) {
this.tableLoadingState = false;
if (data) {
this.pastcfps = data;
console.log(' type of data =====| ' + typeof data);
console.log(' type of data 0 =====| ' + typeof data[0]);
console.log(' type of data =====| ' + typeof this.pastcfps);
console.log(' type of data 0 =====| ' + typeof this.pastcfps[0]);
let dataString = JSON.stringify(data[0]);
console.log(' dataString ========| ' + dataString);
console.log(' Name ========| ' + data[0].Name);
console.log(' Status__c ========| ' + data[0].Status__c);
console.log(' CreatedDate ========| ' + data[0].CreatedDate);
console.log(' Proposal_Submission_Deadline__c ========| ' + data[0].Proposal_Submission_Deadline__c);
this.error = undefined;
if(this.pastcfps.length == 0)
this.offset= this.Prevoffset;
} else if (error) {
this.error = error;
this.pastcfps = undefined;
}
}
handlePrev (_event) {
//window.clearTimeout(this.delayTimeout);
if(this.offset - this.limit >=0)
{
this.tableLoadingState = true;
this.Prevoffset=this.offset;
this.offset = this.offset - this.limit;
}
}
handleNext (_event) {
//window.clearTimeout(this.delayTimeout);
this.tableLoadingState = true;
this.Prevoffset=this.offset;
this.offset = this.offset + this.limit;
}
redirect(event) {
var id = event.currentTarget.dataset.id;
if (id) {
window.location = 'call-for-proposal/' + id;
}
}
sort(e) {
if (this.sortedColumn === e.currentTarget.dataset.id) {
this.sortedDirection = this.sortedDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortedDirection = 'asc';
}
let table = JSON.parse(JSON.stringify(this.pastcfps));
var reverse = this.sortedDirection === 'asc' ? 1 : -1;
try {
table.sort((a, b) => { return new Date(a[e.currentTarget.dataset.id]) > new Date(b[e.currentTarget.dataset.id]) ? 1 * reverse : -1 * reverse });
} catch (error) {
console.log(error);
}
this.sortedColumn = e.currentTarget.dataset.id;
this.pastcfps = table;
if (e.currentTarget.dataset.id == 'Proposal_Submission_Deadline__c') {
let existingIcon = this.template.querySelectorAll('img[data-id="sortIconFp"]');
if (existingIcon[0]) {
existingIcon[0].parentNode.removeChild(existingIcon[0]);
}
let nodes = this.template.querySelectorAll('a[data-id="' + e.currentTarget.dataset.id + '"]');
var icon = document.createElement('IMG');
if (this.sortedDirection === 'asc') { icon.setAttribute('src', this.sortArrowUp); }
if (this.sortedDirection === 'desc') { icon.setAttribute('src', this.sortArrowDown); }
icon.setAttribute('data-id', 'sortIconFp');
if (nodes[0]) { nodes[0].appendChild(icon); }
}
}
}
#AuraEnabled(cacheable=true)
public static List<Call_for_Proposal__c> getPastCfPs(Integer offset, Integer l){
String recordTypeName = [SELECT Id, Name, Status__c, CreatedDate, RecordTypeId, RecordType.Name, Proposal_Submission_Deadline__c FROM Call_for_Proposal__c WHERE Status__c = 'Closed' ].get(0).RecordType.Name;
system.debug('recordTypeName === ' + recordTypeName);
return [SELECT Id, Name, Status__c, CreatedDate, RecordTypeId, RecordType.Name, Proposal_Submission_Deadline__c FROM Call_for_Proposal__c WHERE Status__c = 'Closed' limit :l offset :offset];
}

How to select all checkboxes in JHipster

I created test Spring Boot + AngularJS app to test checkboxes:
html:
... <thead>
<tr>
<th><input type="checkbox" ng-model="isAllSelected"
ng-click="selectAll()"></th>
<th>Lp.</th>
<th>ID</th>
<th>Name</th>
<th>Parent Id</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="test in tests">
<td><input type="checkbox" ng-model="test.checked"
ng-change="optionSelected()" /></td>
<td>{{$index+1}}.</td>
<td>{{test.id}}</td>
<td>{{test.name}}</td>
<td>{{test.parentId}}...
test_controller.js:
(function(angular) {
var AppTestController = function($scope, Test) {
var vm = this;
vm.tests = [];
vm.loadAll = loadAll;
loadAll();
function loadAll() {
Test.query(function(result) {
vm.tests = result;
});
}
vm.selectAll = function() {
var toggleStatus = vm.isAllSelected;
angular.forEach(vm.tests, function(itm) {
itm.checked = toggleStatus;
});
}
vm.optionSelected = function() {
vm.isAllSelected = vm.tests
.every(function(itm) {
return itm.checked;
})
}
};
AppTestController.$inject = [ '$scope', 'Test' ];
angular.module("myApp.test_controller").controller(
"AppTestController", AppTestController);
}(angular));
This works for me as spring Boot app, but when I do the same in JHipster it doesn't work.
How can I get it to work in JHipster?
This is what I currently used and it works!:
.html
<thead>
<tr jh-sort="vm.predicate" ascending="vm.reverse" callback="vm.transition()">
<!--th jh-sort-by="id"><span data-translate="global.field.id">ID</span> <span class="glyphicon glyphicon-sort"></span></th-->
<th><input type="checkbox" icheck ng-change="vm.selectAll()" ng-model="vm.checkAll[vm.page]"></th>
<th jh-sort-by="id"><span data-translate="global.field.id">ID</span></th>
---
</tr>
</thead>
<tbody>
<tr ng-repeat="school in vm.schools track by school.id">
<td><input type="checkbox" icheck ng-model="vm.checkboxes[school.id]" ng-change="vm.select(school)"/></td>
<td>{{($index + 1) + (vm.page - 1) * vm.itemsPerPage}}</td>
...
</tr>
</tbody>
.js
vm.checkAll = [];
var map = {};
vm.checkboxes = [];
vm.selectedItems = [];
vm.selectAll = selectAll;
vm.select = select;
function selectAll () {
var value = vm.checkAll[vm.page];
angular.forEach(vm.schools, function(item) {
if (angular.isDefined(item.id)) {
if(vm.checkboxes[item.id] != value) {
vm.checkboxes[item.id] = value;
vm.select(item);
}
}
});
};
function select (item) {
var value = vm.checkboxes[item.id];
if(value) {
vm.selectedItems.push(item);
if(map[vm.page] == null) map[vm.page] = 1;
else map[vm.page] = map[vm.page] + 1;
if(map[vm.page] == vm.schools.length) {
vm.checkAll[vm.page] = true;
}
} else {
vm.selectedItems.splice(item, 1);
if(map[vm.page] == null) map[vm.page] = 0;
else map[vm.page] = map[vm.page] - 1;
if(map[vm.page] < vm.schools.length) {
vm.checkAll[vm.page] = false;
}
}
};

Outdated Browser - Only show banner from IE8 and lower. Currently IE9 is included

I want this code to only display outdated browser from IE8 and lower. Currently it also shows a message for IE9, which I want to exclude. I did try to remove the IE9 part of the code from the lowerthan part of the code, but no luck.
I got the code from: https://github.com/burocratik/outdated-browser
/*!--------------------------------------------------------------------
JAVASCRIPT "Outdated Browser"
Version: 1.1.0 - 2014
author: Burocratik
website: http://www.burocratik.com
* #preserve
-----------------------------------------------------------------------*/
var outdatedBrowser = function(options) {
//Variable definition (before ajax)
var outdated = document.getElementById("outdated");
// Default settings
this.defaultOpts = {
bgColor: '#f25648',
color: '#ffffff',
lowerThan: 'transform',
languagePath: '../outdatedbrowser/lang/en.html'
}
if (options) {
//assign css3 property to IE browser version
if(options.lowerThan == 'IE8' || options.lowerThan == 'borderSpacing') {
options.lowerThan = 'borderSpacing';
} else if (options.lowerThan == 'IE9' || options.lowerThan == 'boxShadow') {
options.lowerThan = 'boxShadow';
} else if (options.lowerThan == 'IE10' || options.lowerThan == 'transform' || options.lowerThan == '' || typeof options.lowerThan === "undefined") {
options.lowerThan = 'transform';
} else if (options.lowerThan == 'IE11' || options.lowerThan == 'borderImage') {
options.lowerThan = 'borderImage';
}
//all properties
this.defaultOpts.bgColor = options.bgColor;
this.defaultOpts.color = options.color;
this.defaultOpts.lowerThan = options.lowerThan;
this.defaultOpts.languagePath = options.languagePath;
bkgColor = this.defaultOpts.bgColor;
txtColor = this.defaultOpts.color;
cssProp = this.defaultOpts.lowerThan;
languagePath = this.defaultOpts.languagePath;
} else {
bkgColor = this.defaultOpts.bgColor;
txtColor = this.defaultOpts.color;
cssProp = this.defaultOpts.lowerThan;
languagePath = this.defaultOpts.languagePath;
};//end if options
//Define opacity and fadeIn/fadeOut functions
var done = true;
function function_opacity(opacity_value) {
outdated.style.opacity = opacity_value / 100;
outdated.style.filter = 'alpha(opacity=' + opacity_value + ')';
}
// function function_fade_out(opacity_value) {
// function_opacity(opacity_value);
// if (opacity_value == 1) {
// outdated.style.display = 'none';
// done = true;
// }
// }
function function_fade_in(opacity_value) {
function_opacity(opacity_value);
if (opacity_value == 1) {
outdated.style.display = 'block';
}
if (opacity_value == 100) {
done = true;
}
}
//check if element has a particular class
// function hasClass(element, cls) {
// return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;
// }
var supports = (function() {
var div = document.createElement('div'),
vendors = 'Khtml Ms O Moz Webkit'.split(' '),
len = vendors.length;
return function(prop) {
if ( prop in div.style ) return true;
prop = prop.replace(/^[a-z]/, function(val) {
return val.toUpperCase();
});
while(len--) {
if ( vendors[len] + prop in div.style ) {
return true;
}
}
return false;
};
})();
//if browser does not supports css3 property (transform=default), if does > exit all this
if ( !supports(''+ cssProp +'') ) {
if (done && outdated.style.opacity !== '1') {
done = false;
for (var i = 1; i <= 100; i++) {
setTimeout((function (x) {
return function () {
function_fade_in(x);
};
})(i), i * 8);
}
}
}else{
return;
};//end if
//Check AJAX Options: if languagePath == '' > use no Ajax way, html is needed inside <div id="outdated">
if( languagePath === ' ' || languagePath.length == 0 ){
startStylesAndEvents();
}else{
grabFile(languagePath);
}
//events and colors
function startStylesAndEvents(){
var btnClose = document.getElementById("btnCloseUpdateBrowser");
var btnUpdate = document.getElementById("btnUpdateBrowser");
//check settings attributes
outdated.style.backgroundColor = bkgColor;
//way too hard to put !important on IE6
outdated.style.color = txtColor;
outdated.children[0].style.color = txtColor;
outdated.children[1].style.color = txtColor;
//check settings attributes
btnUpdate.style.color = txtColor;
// btnUpdate.style.borderColor = txtColor;
if (btnUpdate.style.borderColor) btnUpdate.style.borderColor = txtColor;
btnClose.style.color = txtColor;
//close button
btnClose.onmousedown = function() {
outdated.style.display = 'none';
return false;
};
//Override the update button color to match the background color
btnUpdate.onmouseover = function() {
this.style.color = bkgColor;
this.style.backgroundColor = txtColor;
};
btnUpdate.onmouseout = function() {
this.style.color = txtColor;
this.style.backgroundColor = bkgColor;
};
}//end styles and events
// IF AJAX with request ERROR > insert english default
var ajaxEnglishDefault = '<h6>Your browser is out-of-date!</h6>'
+ '<p>Update your browser to view this website correctly. <a id="btnUpdateBrowser" href="http://outdatedbrowser.com/">Update my browser now </a></p>'
+ '<p class="last">×</p>';
//** AJAX FUNCTIONS - Bulletproof Ajax by Jeremy Keith **
function getHTTPObject() {
var xhr = false;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {
xhr = false;
}
}
}
return xhr;
};//end function
function grabFile(file) {
var request = getHTTPObject();
if (request) {
request.onreadystatechange = function() {
displayResponse(request);
};
request.open("GET", file, true);
request.send(null);
}
return false;
};//end grabFile
function displayResponse(request) {
var insertContentHere = document.getElementById("outdated");
if (request.readyState == 4) {
if (request.status == 200 || request.status == 304) {
insertContentHere.innerHTML = request.responseText;
}else{
insertContentHere.innerHTML = ajaxEnglishDefault;
}
startStylesAndEvents();
}
return false;
};//end displayResponse
////////END of outdatedBrowser function
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Outdated Browser</title>
<meta name="description" content="A time saving tool for developers. It detects outdated browsers and advises users to upgrade to a new version.">
<!-- Styles -->
<link rel="stylesheet" href="../outdatedbrowser/outdatedbrowser.min.css">
<link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<style type="text/css">
body {
font-family: 'Open Sans', sans-serif; text-align: center;
background-color: #fafafa; color: #0a0a0a; line-height: 1.5em;
}
h1{font-size: 2.6em; line-height: 2em;}
h3, h4{line-height: 1em; margin: 2.5em 0 -.4em 0; text-transform: uppercase;}
h3 span, h4 span{text-transform: none;}
p{padding-bottom: 1em;}
p.designBy{position: absolute; bottom: 0; right: 1em; font-size: .8em;}
a {color: #0a0a0a;}
ul{list-style-type: none; padding: 0;}
</style>
</head>
<body>
<!-- ============= YOUR CONTENT ============= -->
<h1>Outdated Browser</h1>
<p>Remember: If you can't see the message, it's a good thing! You are using a modern browser :D</p>
<h3>DEFAULT properties but using jQuery (must support IE6+)</h3>
<p>bgColor: '#f25648', color: '#ffffff', lowerThan: 'transform' (<IE10), languagePath: 'your_path/outdatedbrowser/lang/en.html'</p>
<h3>What does it look like? <span>(it may differ in your tests) </span></h3>
<ul>
<li>IE7 - VISTA</li>
</ul>
<p class="designBy">by Bürocratik</p>
<!-- ============= Outdated Browser ============= -->
<div id="outdated"></div>
<!-- javascript includes -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script src="../outdatedbrowser/outdatedbrowser.js"></script>
<!-- plugin call -->
<script>
//USING jQuery
$(document).ready(function() {
outdatedBrowser({
bgColor: '#f25648',
color: '#ffffff',
lowerThan: 'transform',
languagePath: '../outdatedbrowser/lang/en.html'
})
})
console.log(outdatedBrowser);
</script>
</script>
</body>
</html>

Create a new Webshop Parameter

I am very new to hybris e-commerce software and trying to learn with the help of wiki documents provided with it.I am trying to create a new Webshop parameter with three options so I can choose any one of then by HMC. I have changed the items.xml and created a enum and a new attribute in Basestore but on HMC name of new parameter is not proper its taking qualifier name appended with "[" can anyone suggest why it is happening and how to define name of new parameter.
1) myextension-items.xml
<itemtype code="MyType" extends="BaseType">
<attributes>
<attribute qualifier="attr1" type="java.lang.String">
<persistence type="property" />
</attribute>
</attributes>
</itemtype>
2) localization\myextension-locales_en.properties (or other: _fr, _de, etc)
type.MyType.name=My Type name
type.MyType.description=My Type description
type.MyType.attr1.name=Attribute 1 name
type.MyType.attr1.description=Attribute 1 description
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
table {
border: 1px solid black;
}
p {
position: relative;
left: 60px;
}
</style>
</head>
<body>
<h2>Pizzahut</h2>
<div id="container">
<label>Megnevezés</label><br>
<input id='Megnevezes' type="text"><br>
<label>Pizza mérete</label><br>
<select id="pizzameret" name="pizzameret" >
<option>S</option>
<option>L</option>
<option>XL</option>
</select><br>
<label>Egységár</label><br>
<input id='Egysegar' type="number"><br>
<label>Fénykép</label><br>
<input id='fenykep' type="text"><br>
<button onclick="pizzaHozzaad()">Feltöltés</button>
<!--fügvényhívás-->
</div>
<br>
<br>
<br>
<div id="tablaParent">
</div>
<br>
<br>
<br>
<div id="kosarTartalma">
<p>Kosár tartalma</p>
<table>
<thead>
<tr>
<th>id</th>
<th>pizzameret</th>
<th>megnevezes</th>
<th>egysegar</th>
<th>fenykep</th>
</tr>
<!--
<tr>
<th><input id="idSzuro" /></th>
<th>
<select id="pizzameretSzuro">
<option>S</option>
<option>L</option>
<option>XL</option>
</select>
</th>
<th><input id="megnevezesSzuro" /></th>
<th><input id="egysegarSzuro" /></th>
<th><input id="fenykepSzuro" /></th>
</tr>
-->
</thead>
<tbody id="kosarBody">
</tbody>
</table>
</div>
<script type="text/javascript" , src="pizza.js"></script>
<script type="text/javascript" , src="webshoplogika.js"></script>
</body>
</html>
function Pizza(id,megnevezes,egysegar,fenykep,pizzameret)
{
this.id=id;
this.pizzameret=pizzameret;
this.megnevezes = megnevezes;
this.egysegar = egysegar;
this.fenykep = fenykep ;
}
"use strict"
var termekek = [];
var tablazatParent = "";
var kosar = [];
var novekvo = true;
termekek.push(new Pizza(1, "pizza1", 600, "kamu.jpg","xl"));
termekek.push(new Pizza(2, "pizza2", 100, "kamu.jpg","xl"));
termekek.push(new Pizza(3, "pizza3", 200, "kamu.jpg","xl"));
termekek.push(new Pizza(4, "pizza4", 4000, "kamu.jpg","xl"));
termekek.push(new Pizza(5, "pizza5", 5000, "kamu.jpg","xl"));
termekek.push(new Pizza(6, "pizza5", 12000, "kamu.jpg","xl"));
var pizzameret = document.getElementById("pizzameret");
var Megnevezes = document.getElementById("Megnevezes");
var Egysegar = document.getElementById("Egysegar");
var fenykep = document.getElementById("fenykep");
var id = 6;
window.addEventListener('load', Window_Load_Handler, false);
function Window_Load_Handler() {
tablazatParent=document.getElementById("tablaParent");
tablazatRajzoloFuggveny(termekek);
}
function pizzaHozzaad() {
id++;
var pizzaNev = Megnevezes.value;
var pizzaAr = Egysegar.value;
var fenykepURL = fenykep.value;
var ujPizza = new Pizza(id, pizzaNev, pizzaAr, fenykepURL);
termekek.push(ujPizza);
console.log(ujPizza);
tablazatFrissites();
}
function tablazatRajzoloFuggveny(inputArray) { /* ugy kell meghivni, masik gombnyomasra,hogy at kell adnunk neki paratmeterkent egy tombot,
amivel megmondjuk melyik tombbol szeretnenk tablazatot csinalni */
var tablazat = document.createElement('table')
tablazat.setAttribute("border", "true"); /* a tablazatot hozza letre */
var sor = document.createElement('TR'); /* a sort hozza letre */
for (var k in inputArray[0]) {
var cella = document.createElement('TH') /*create elementtel keszitettunk egy header cellat */
cella.innerText = k;
cella.addEventListener('click', rendezd , false);
sor.appendChild(cella); /* cellat felakasztja a sorra */
}
var btnCella = document.createElement('TH'); // gombot kreálunk
btnCella.innerText = "Kosárba";
sor.appendChild(btnCella);
tablazat.appendChild(sor);
var btnCella = document.createElement('TH'); // gombot kreálunk
btnCella.innerText = "Törlés";
sor.appendChild(btnCella);
tablazat.appendChild(sor);
var sor = document.createElement('TR');
for (var k in inputArray[0]) {
var cella = document.createElement('TH') /*create elementtel keszitettunk egy header cellat */
if (k == "pizzameret") {
var szuro = document.createElement('SELECT');
szuro.id= k
var optionXL = document.createElement('OPTION')
optionXL.text = 'XL'
optionXL.value = 'XL'
var optionL = document.createElement('OPTION')
optionL.text = 'L'
optionL.value = 'L'
var optionS = document.createElement('OPTION')
optionS.text = 'S'
optionS.value = 'S'
szuro.appendChild(optionXL)
szuro.appendChild(optionL)
szuro.appendChild(optionS)
szuro.addEventListener('change', szurd , true);
cella.appendChild(szuro)
} else {
var szuro = document.createElement('INPUT');
szuro.id= k
szuro.addEventListener('focusout', szurd , true);
cella.appendChild(szuro)
}
sor.appendChild(cella); /* cellat felakasztja a sorra */
}
tablazat.appendChild(sor); /* sort felakasztja a tablara */
document.body.appendChild(tablazat);
for (var i = 0; i < inputArray.length; i++) {
var sor = document.createElement('TR'); /* a sort hozza letre */
for (var k in inputArray[i]) {
var cella = document.createElement('TD') /*create elementtel keszitettunk egy header cellat */
cella.innerText = inputArray[i][k]; /*aktualis auto k kulcsai, value-jat olvassa be */
sor.appendChild(cella);
}
var btnCella = document.createElement('TD'); // gombot kreálunk
btnCella.innerHTML = "";
sor.appendChild(btnCella);
tablazat.appendChild(sor);
tablazat.appendChild(sor);
var kosarbaBtn = document.createElement('input');
kosarbaBtn.setAttribute('type', 'button');
kosarbaBtn.setAttribute('value', 'Kosárba');
kosarbaBtn.setAttribute('termekID',inputArray[i].id );
kosarbaBtn.addEventListener('click', Kosarba_Click_Handler , false);
var torolBtn = document.createElement('input');
torolBtn.setAttribute('type', 'button');
torolBtn.setAttribute('value', 'Torlés');
torolBtn.setAttribute('termekID',inputArray[i].id );
torolBtn.addEventListener('click', toroldasort , false);
btnCella.appendChild( kosarbaBtn);
var btnCella = document.createElement('TD'); // gombot kreálunk
btnCella.appendChild( torolBtn);
sor.appendChild(btnCella);
tablazat.appendChild(sor);
tablazat.appendChild(sor);
}
tablazatParent.appendChild(tablazat);
}
function toroldasort(){
var btn = event.target;
var id = btn.getAttribute('termekID');
console.log("termek id:" + btn.getAttribute('termekID'));
for( var i = 0; i < termekek.length; i++){
if ( termekek[i].id == id ) {
console.log("lofasz");
termekek.splice(i, 1);
break;
}
}
tablazatFrissites();
}
function tablazatFrissites() {
//var tablazatParent=getElementById('tablazat'); //feltetteük a tetejére
var nodeTermekLista = document.querySelector('#tablaParent');
nodeTermekLista.innerText='';
tablazatRajzoloFuggveny(termekek);
}
function rendezd() {
var header = event.target
var attr = header.innerText
console.log(attr); // ha igy mukodik akkor nem is kell gomb se input type legyen stb. próba cseresznye
// fasza, megy, akkor folytassuk, ugye rendezni kell a szerint az attributum szerint amit megnyomtunk
// ez most csökkenő sorrendbe rendezi az oszlopokat (már amennyi értelme van ennek stringek esetében)
termekek.sort(function(a, b){
//console.log(a[attr])
// na mindegy, eddigiek alapján a ciklussal láttuk h kell kiszedni egy ojjektum mezejét
// á faszom, kell erre legyen vmi egyszerű megoldás
console.log(a[attr] +' '+ b[attr])
// ez ugye számokra jó de stringekre nem
if (novekvo) {
if (a[attr] < b[attr])
return -1;
if (a[attr] > b[attr])
return 1;
} else {
if (a[attr] < b[attr])
return 1;
if (a[attr] > b[attr])
return -1;
}
return 0;
})
novekvo = !novekvo
console.log(novekvo)
// https://stackoverflow.com/questions/1129216/sort-array-of-objects-by-string-property-value
// fasza, műxik, látszik a termékeken h most csökkenő sorrendben vannak
// ja igen és még ki kell rajzolni
tablazatFrissites()
}
function szurd() {
var szurtTermekek = []
var asd = event.target
console.log(termekek)
for (var i = 0; i < termekek.length; i++) {
console.log(termekek[i][asd.id]+' '+asd.value)
if (termekek[i][asd.id] == asd.value) {
console.log(termekek[i][asd.id])
szurtTermekek.push(termekek[i])
}
}
console.log(szurtTermekek)
if (szurtTermekek.length > 0) {
var nodeTermekLista = document.querySelector('#tablaParent');
nodeTermekLista.innerText='';
tablazatRajzoloFuggveny(szurtTermekek)
}
}
function Kosarba_Click_Handler() {
var btn = event.target;
var id = btn.getAttribute('termekID');
console.log("kosarba termek id:" + btn.getAttribute('termekID'));
var valasztottTermek;
for( var i = 0; i < termekek.length; i++){
if ( termekek[i].id == id ) {
valasztottTermek = termekek[i];
console.log("kosarba!!");
break;
}
}
var kosarLista = document.querySelector('#kosarBody');
var sor = document.createElement('TR');
for (var k in valasztottTermek) {
var cella = document.createElement('TD')
cella.innerText = valasztottTermek[k];
sor.appendChild(cella);
}
kosarLista.appendChild(sor);
}

Undo-Redo feature in Fabric.js

Is there any built-in support for for undo/redo in Fabric.js? Can you please guide me on how you used this cancel and repeat in [http://printio.ru/][1]
In http://jsfiddle.net/SpgGV/9/, move the object and change its size. If the object state is changed, and then we do undo/redo, its previous state will be deleted when the next change comes. It makes it easier to do undo/redo. All events of canvas should be called before any element is added to canvas. I didn't add an object:remove event here. You can add it yourself. If one element is removed, the state and list should be invalid if this element is in this array. The simpler way is to set state and list = [] and index = 0.
This will clear the state of your undo/redo queue. If you want to keep all states, such as add/remove, my suggestion is to add more properties to the element of your state array. For instance, state = [{"data":object.originalState, "event": "added"}, ....]. The "event" could be "modified" or "added" and set in a corresponding event handler.
If you have added one object, then set state[index].event="added" so that next time, when you use undo, you check it. If it's "added", then remove it anyway. Or when you use redo, if the target one is "added", then you added it. I've recently been quite busy. I will add codes to jsfiddle.net later.
Update: added setCoords() ;
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
canvas.on("object:added", function (e) {
var object = e.target;
console.log('object:modified');
if (action === true) {
state = [state[index2]];
list = [list[index2]];
action = false;
console.log(state);
index = 1;
}
object.saveState();
console.log(object.originalState);
state[index] = JSON.stringify(object.originalState);
list[index] = object;
index++;
index2 = index - 1;
refresh = true;
});
canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
if (action === true) {
state = [state[index2]];
list = [list[index2]];
action = false;
console.log(state);
index = 1;
}
object.saveState();
state[index] = JSON.stringify(object.originalState);
list[index] = object;
index++;
index2 = index - 1;
console.log(state);
refresh = true;
});
function undo() {
if (index <= 0) {
index = 0;
return;
}
if (refresh === true) {
index--;
refresh = false;
}
console.log('undo');
index2 = index - 1;
current = list[index2];
current.setOptions(JSON.parse(state[index2]));
index--;
current.setCoords();
canvas.renderAll();
action = true;
}
function redo() {
action = true;
if (index >= state.length - 1) {
return;
}
console.log('redo');
index2 = index + 1;
current = list[index2];
current.setOptions(JSON.parse(state[index2]));
index++;
current.setCoords();
canvas.renderAll();
}
Update: better solution to take edit history algorithm into account. Here we can use Editing.getInst().set(item) where the item could be {action, object, state}; For example, {"add", object, "{JSON....}"}.
/**
* Editing : we will save element states into an queue, and the length of queue
* is fixed amount, for example, 0..99, each element will be insert into the top
* of queue, queue.push, and when the queue is full, we will shift the queue,
* to remove the oldest element from the queue, queue.shift, and then we will
* do push.
*
* So the latest state will be at the top of queue, and the oldest one will be
* at the bottom of the queue (0), and the top of queue is changed, could be
* 1..99.
*
* The initialized action is "set", it will insert item into the top of queue,
* even if it arrived the length of queue, it will queue.shift, but still do
* the same thing, and queue only abandon the oldest element this time. When
* the current is changed and new state is coming, then this time, top will be
* current + 1.
*
* The prev action is to fetch "previous state" of the element, and it will use
* "current" to do this job, first, we will --current, and then we will return
* the item of it, because "current" always represent the "current state" of
* element. When the current is equal 0, that means, we have fetched the last
* element of the queue, and then it arrived at the bottom of the queue.
*
* The next action is to fetch "next state" after current element, and it will
* use "current++" to do the job, when the current is equal to "top", it means
* we have fetched the latest element, so we should stop.
*
* If the action changed from prev/next to "set", then we should reset top to
* "current", and abandon all rest after that...
*
* Here we should know that, if we keep the reference in the queue, the item
* in the queue will never be released.
*
*
* #constructor
*/
function Editing() {
this.queue = [];
this.length = 4;
this.bottom = 0;
this.top = 0;
this.current = 0;
this.empty = true;
// At the Begin of Queue
this.BOQ = true;
// At the End of Queue
this.EOQ = true;
// 0: set, 1: prev, 2: next
this._action = 0;
this._round = 0;
}
Editing.sharedInst = null;
Editing.getInst = function (owner) {
if (Editing.sharedInst === null) {
Editing.sharedInst = new Editing(owner);
}
return Editing.sharedInst;
};
/**
* To set the item into the editing queue, and mark the EOQ, BOQ, so we know
* the current position.
*
* #param item
*/
Editing.prototype.set = function (item) {
console.log("=== Editing.set");
var result = null;
if (this._action != 0) {
this.top = this.current + 1;
}
if (this.top >= this.length) {
result = this.queue.shift();
this.top = this.length - 1;
}
this._action = 0;
this.queue[this.top] = item;
this.current = this.top;
this.top++;
this.empty = false;
this.EOQ = true;
this.BOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return result;
};
/**
* To fetch the previous item just before current one
*
* #returns {item|boolean}
*/
Editing.prototype.prev = function () {
console.log("=== Editing.prev");
if (this.empty) {
return false;
}
if (this.BOQ) {
return false;
}
this._action = 1;
this.current--;
if (this.current == this.bottom) {
this.BOQ = true;
}
var item = this.queue[this.current];
this.EOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return item;
};
/**
* To fetch the next item just after the current one
*
* #returns {*|boolean}
*/
Editing.prototype.next = function () {
console.log("=== Editing.next");
if (this.empty) {
return false;
}
if (this.EOQ) {
return false;
}
this.current++;
if (this.current == this.top - 1 && this.top < this.length) {
this.EOQ = true;
}
if (this.current == this.top - 1 && this.top == this.length) {
this.EOQ = true;
}
this._action = 2;
var item = this.queue[this.current];
this.BOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return item;
};
/**
* To empty the editing and reset all state
*/
Editing.prototype.clear = function () {
this.queue = [];
this.bottom = 0;
this.top = 0;
this.current = 0;
this.empty = true;
this.BOQ = true;
this.EOQ = false;
};
Here is a solution that started with this simpler answer to the similar question, Undo Redo History for Canvas FabricJs.
My answer is along the same lines as Tom's answer and the other answers that are modifications of Tom's answer.
To track the state, I'm using JSON.stringify(canvas) and canvas.loadFromJSON() like the other answers and have an event registered on the object:modified to capture the state.
One important thing is that the final canvas.renderAll() should be called in a callback passed to the second parameter of loadFromJSON(), like this
canvas.loadFromJSON(state, function() {
canvas.renderAll();
}
This is because it can take a few milliseconds to parse and load the JSON and you need to wait until that's done before you render. It's also important to disable the undo and redo buttons as soon as they're clicked and to only re-enable in the same call back. Something like this
$('#undo').prop('disabled', true);
$('#redo').prop('disabled', true);
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn buttons back on appropriately
...
(see full code below)
}
I have an undo and a redo stack and a global for the last unaltered state. When some modification occurs, then the previous state is pushed into the undo stack and the current state is re-captured.
When the user wants to undo, then current state is pushed to the redo stack. Then I pop off the last undo and both set it to the current state and render it on the canvas.
Likewise when the user wants to redo, the current state is pushed to the undo stack. Then I pop off the last redo and both set it to the current state and render it on the canvas.
The Code
// Fabric.js Canvas object
var canvas;
// current unsaved state
var state;
// past states
var undo = [];
// reverted states
var redo = [];
/**
* Push the current state into the undo stack and then capture the current state
*/
function save() {
// clear the redo stack
redo = [];
$('#redo').prop('disabled', true);
// initial call won't have a state
if (state) {
undo.push(state);
$('#undo').prop('disabled', false);
}
state = JSON.stringify(canvas);
}
/**
* Save the current state in the redo stack, reset to a state in the undo stack, and enable the buttons accordingly.
* Or, do the opposite (redo vs. undo)
* #param playStack which stack to get the last state from and to then render the canvas as
* #param saveStack which stack to push current state into
* #param buttonsOn jQuery selector. Enable these buttons.
* #param buttonsOff jQuery selector. Disable these buttons.
*/
function replay(playStack, saveStack, buttonsOn, buttonsOff) {
saveStack.push(state);
state = playStack.pop();
var on = $(buttonsOn);
var off = $(buttonsOff);
// turn both buttons off for the moment to prevent rapid clicking
on.prop('disabled', true);
off.prop('disabled', true);
canvas.clear();
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn the buttons back on if applicable
on.prop('disabled', false);
if (playStack.length) {
off.prop('disabled', false);
}
});
}
$(function() {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the canvas
canvas = new fabric.Canvas('canvas');
canvas.setWidth(500);
canvas.setHeight(500);
// save initial state
save();
// register event listener for user's actions
canvas.on('object:modified', function() {
save();
});
// draw button
$('#draw').click(function() {
var imgObj = new fabric.Circle({
fill: '#' + Math.floor(Math.random() * 16777215).toString(16),
radius: Math.random() * 250,
left: Math.random() * 250,
top: Math.random() * 250
});
canvas.add(imgObj);
canvas.renderAll();
save();
});
// undo and redo buttons
$('#undo').click(function() {
replay(undo, redo, '#redo', this);
});
$('#redo').click(function() {
replay(redo, undo, '#undo', this);
})
});
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" type="text/javascript"></script>
</head>
<body>
<button id="draw">circle</button>
<button id="undo" disabled>undo</button>
<button id="redo" disabled>redo</button>
<canvas id="canvas" style="border: solid 1px black;"></canvas>
</body>
I am allowing the user to remove the last added path (in my painting application), this works fine for me:
var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);
if(item.get('type') === 'path') {
fabricCanvas.remove(item);
fabricCanvas.renderAll();
}
But you could also remove the IF statement and let people remove anything.
I know its late to answer this but this is my version of implementing this. Can be useful to someone.
I have implemented this feature by saving Canvas States as JSON. Whenever a user adds or modifies an object in the Canvas, it will save the changed canvas state and maintain it in an array. This array is then manipulated whenever user clicks on Undo or Redo button.
Take a look at this link. I have also provided a working Demo URL.
https://github.com/abhi06991/Undo-Redo-Fabricjs
HTML:
<canvas id="canvas" width="400" height="400"></canvas>
<button type="button" id="undo" >Undo</button>
<button type="button" id="redo" disabled>Redo</button>
JS:
var canvasDemo = (function(){
var _canvasObject = new fabric.Canvas('canvas',{backgroundColor : "#f5deb3"});
var _config = {
canvasState : [],
currentStateIndex : -1,
undoStatus : false,
redoStatus : false,
undoFinishedStatus : 1,
redoFinishedStatus : 1,
undoButton : document.getElementById('undo'),
redoButton : document.getElementById('redo'),
};
_canvasObject.on(
'object:modified', function(){
updateCanvasState();
}
);
_canvasObject.on(
'object:added', function(){
updateCanvasState();
}
);
var addObject = function(){
var rect = new fabric.Rect({
left : 100,
top : 100,
fill : 'red',
width : 200,
height : 200
});
_canvasObject.add(rect);
_canvasObject.setActiveObject(rect);
_canvasObject.renderAll();
}
var updateCanvasState = function() {
if((_config.undoStatus == false && _config.redoStatus == false)){
var jsonData = _canvasObject.toJSON();
var canvasAsJson = JSON.stringify(jsonData);
if(_config.currentStateIndex < _config.canvasState.length-1){
var indexToBeInserted = _config.currentStateIndex+1;
_config.canvasState[indexToBeInserted] = canvasAsJson;
var numberOfElementsToRetain = indexToBeInserted+1;
_config.canvasState = _config.canvasState.splice(0,numberOfElementsToRetain);
}else{
_config.canvasState.push(canvasAsJson);
}
_config.currentStateIndex = _config.canvasState.length-1;
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}
}
}
var undo = function() {
if(_config.undoFinishedStatus){
if(_config.currentStateIndex == -1){
_config.undoStatus = false;
}
else{
if (_config.canvasState.length >= 1) {
_config.undoFinishedStatus = 0;
if(_config.currentStateIndex != 0){
_config.undoStatus = true;
_canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex-1],function(){
var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex-1]);
_canvasObject.renderAll();
_config.undoStatus = false;
_config.currentStateIndex -= 1;
_config.undoButton.removeAttribute("disabled");
if(_config.currentStateIndex !== _config.canvasState.length-1){
_config.redoButton.removeAttribute('disabled');
}
_config.undoFinishedStatus = 1;
});
}
else if(_config.currentStateIndex == 0){
_canvasObject.clear();
_config.undoFinishedStatus = 1;
_config.undoButton.disabled= "disabled";
_config.redoButton.removeAttribute('disabled');
_config.currentStateIndex -= 1;
}
}
}
}
}
var redo = function() {
if(_config.redoFinishedStatus){
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}else{
if (_config.canvasState.length > _config.currentStateIndex && _config.canvasState.length != 0){
_config.redoFinishedStatus = 0;
_config.redoStatus = true;
_canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex+1],function(){
var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex+1]);
_canvasObject.renderAll();
_config.redoStatus = false;
_config.currentStateIndex += 1;
if(_config.currentStateIndex != -1){
_config.undoButton.removeAttribute('disabled');
}
_config.redoFinishedStatus = 1;
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}
});
}
}
}
}
return {
addObject : addObject,
undoButton : _config.undoButton,
redoButton : _config.redoButton,
undo : undo,
redo : redo,
}
})();
canvasDemo.undoButton.addEventListener('click',function(){
canvasDemo.undo();
});
canvasDemo.redoButton.addEventListener('click',function(){
canvasDemo.redo();
});
canvasDemo.addObject();
My use case was drawing simple shapes akin to blueprints, so I didn't have to worry about the overhead of saving the whole canvas state. If you are in the same situation, this is very easy to accomplish. This code assumes you have a 'wrapper' div around the canvas, and that you want the undo/redo functionality bound to the standard windows keystrokes of 'CTRL+Z' and 'CTRL+Y'.
The purpose of the 'pause_saving' variable was to account for the fact that when a canvas is re-rendered it seemingly created each object one by one all over again, and we don't want to catch these events, as they aren't REALLY new events.
//variables for undo/redo
let pause_saving = false;
let undo_stack = []
let redo_stack = []
canvas.on('object:added', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object added, state saved', undo_stack);
}
});
canvas.on('object:modified', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object modified, state saved', undo_stack);
}
});
canvas.on('object:removed', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object removed, state saved', undo_stack);
}
});
//Listen for undo/redo
wrapper.addEventListener('keydown', function(event){
//Undo - CTRL+Z
if (event.ctrlKey && event.keyCode == 90) {
pause_saving=true;
redo_stack.push(undo_stack.pop());
let previous_state = undo_stack[undo_stack.length-1];
if (previous_state == null) {
previous_state = '{}';
}
canvas.loadFromJSON(previous_state,function(){
canvas.renderAll();
})
pause_saving=false;
}
//Redo - CTRL+Y
else if (event.ctrlKey && event.keyCode == 89) {
pause_saving=true;
state = redo_stack.pop();
if (state != null) {
undo_stack.push(state);
canvas.loadFromJSON(state,function(){
canvas.renderAll();
})
pause_saving=false;
}
}
});
You can use "object:added" and/or "object:removed" for that — fabricjs.com/events
You can follow this post:
Do we have canvas Modified Event in Fabric.js?
I know the answer is already chosen but here is my version, script is condensed, also added a reset to original state. After any event you want to save just call saveState(); jsFiddle
canvas = new fabric.Canvas('canvas', {
selection: false
});
function saveState(currentAction) {
currentAction = currentAction || '';
// if (currentAction !== '' && lastAction !== currentAction) {
$(".redo").val($(".undo").val());
$(".undo").val(JSON.stringify(canvas));
console.log("Saving After " + currentAction);
lastAction = currentAction;
// }
var objects = canvas.getObjects();
for (i in objects) {
if (objects.hasOwnProperty(i)) {
objects[i].setCoords();
}
}
}
canvas.on('object:modified', function (e) {
saveState("modified");
});
// Undo Canvas Change
function undo() {
canvas.loadFromJSON($(".redo").val(), canvas.renderAll.bind(canvas));
}
// Redo Canvas Change
function redo() {
canvas.loadFromJSON($(".undo").val(), canvas.renderAll.bind(canvas));
};
$("#reset").click(function () {
canvas.loadFromJSON($("#original_canvas").val(),canvas.renderAll.bind(canvas));
});
var bgnd = new fabric.Image.fromURL('https://s3-eu-west-1.amazonaws.com/kienzle.dev.cors/img/image2.png', function(oImg){
oImg.hasBorders = false;
oImg.hasControls = false;
// ... Modify other attributes
canvas.insertAt(oImg,0);
canvas.setActiveObject(oImg);
myImg = canvas.getActiveObject();
saveState("render");
$("#original_canvas").val(JSON.stringify(canvas.toJSON()));
});
$("#undoButton").click(function () {
undo();
});
$("#redoButton").click(function () {
redo();
});
i developed a small script for you,hope it will help you .see this demo Fiddle
although redo is not perfect you have to click minimum two time at undo button then redo work .you can easily solve this problem with giving simple conditions in redo code.
//Html
<canvas id="c" width="400" height="200" style=" border-radius:25px 25px 25px 25px"></canvas>
<br>
<br>
<input type="button" id="addtext" value="Add Text"/>
<input type="button" id="undo" value="Undo"/>
<input type="button" id="redo" value="redo"/>
<input type="button" id="clear" value="Clear Canvas"/>
//script
var canvas = new fabric.Canvas('c');
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 50,
top: 30,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
var vall=10;
var l=0;
var flag=0;
var k=1;
var yourJSONString = new Array();
canvas.observe('object:selected', function(e) {
//yourJSONString = JSON.stringify(canvas);
if(k!=10)
{
yourJSONString[k] = JSON.stringify(canvas);
k++;
}
j = k;
var activeObject = canvas.getActiveObject();
});
$("#undo").click(function(){
if(k-1!=0)
{
canvas.clear();
canvas.loadFromJSON(yourJSONString[k-1]);
k--;
l++;
}
canvas.renderAll();
});
$("#redo").click(function(){
if(l > 1)
{
canvas.clear();
canvas.loadFromJSON(yourJSONString[k+1]);
k++;
l--;
canvas.renderAll();
}
});
$("#clear").click(function(){
canvas.clear();
});
$("#addtext").click(function(){
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
});
I have answer to all your queries :) get a smile
check this link.. its all done ... copy & paste it :P
http://jsfiddle.net/SpgGV/27/
var canvas = new fabric.Canvas('c');
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
state[0] = JSON.stringify(canvas.toDatalessJSON());
console.log(JSON.stringify(canvas.toDatalessJSON()));
$("#clear").click(function(){
canvas.clear();
index=0;
});
$("#addtext").click(function(){
++index;
action=true;
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
});
canvas.on("object:added", function (e) {
if(action===true){
var object = e.target;
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[index] = JSON.stringify(canvas.toDatalessJSON());
refresh = true;
action=false;
canvas.renderAll();
}
});
function undo() {
if (index < 0) {
index = 0;
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}
console.log('undo');
canvas.loadFromJSON(state[index]);
console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
action = false;
}
function redo() {
action = false;
if (index >= state.length - 1) {
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}
console.log('redo');
canvas.loadFromJSON(state[index]);
console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
canvas.renderAll();
}
canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[++index] = JSON.stringify(canvas.toDatalessJSON());
action=false;
});
$('#undo').click(function () {
index--;
undo();
});
$('#redo').click(function () {
index++;
redo();
});

Resources