Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Applab: add fit property for images #19011

Merged
merged 5 commits into from
Nov 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
"details-element-polyfill": "https://github.com/javan/details-element-polyfill",
"filesaver.js": "0.2.0",
"jszip": "3.0.0",
"object-fit-images": "^3.2.3",
"query-string": "4.1.0",
"react-dom-confetti": "^0.0.8",
"react-hot-loader": "^1.3.1",
Expand Down
6 changes: 6 additions & 0 deletions apps/src/applab/applab.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {getRandomDonorTwitter} from '../util/twitterHelper';

import {TestResults, ResultType} from '../constants';
import i18n from '../code-studio/i18n';
import {applabObjectFitImages} from './applabObjectFitImages';

/**
* Create a namespace for the application.
Expand Down Expand Up @@ -350,6 +351,11 @@ Applab.init = function (config) {
// Necessary for tests.
thumbnailUtils.init();

// Enable polyfill so we can use object-fit (we must additionally specify
// the style in font-family and avoid scale-down & using it in media queries)
// See https://www.npmjs.com/package/object-fit-images for details.
applabObjectFitImages(null, { watchMQ: true });

// replace studioApp methods with our own
studioApp().reset = this.reset.bind(this);
studioApp().runButtonClick = this.runButtonClick.bind(this);
Expand Down
13 changes: 13 additions & 0 deletions apps/src/applab/applabObjectFitImages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import objectFitImages from 'object-fit-images';

/**
* Wrapper to objectFitImages() to avoid using it on phantomjs
* See https://github.com/bfred-it/object-fit-images/issues/11
*/

export function applabObjectFitImages(imgs, opts) {
if (/PhantomJS/.test(window.navigator.userAgent)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for doing this instead of IN_UNIT_TEST - that means if we switch to headless chrome we'll actually run this polyfill in tests.

return;
}
objectFitImages(imgs, opts);
}
2 changes: 2 additions & 0 deletions apps/src/applab/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ applabCommands.image = function (opts) {
newImage.id = opts.elementId;
newImage.style.position = 'relative';

Applab.updateProperty(newImage, 'objectFit', 'contain');

return Boolean(Applab.activeScreen().appendChild(newImage));
};

Expand Down
47 changes: 47 additions & 0 deletions apps/src/applab/designElements/image.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import ZOrderRow from './ZOrderRow';
import EventHeaderRow from './EventHeaderRow';
import EventRow from './EventRow';
import {ICON_PREFIX_REGEX} from '../constants';
import EnumPropertyRow from './EnumPropertyRow';
import * as elementUtils from './elementUtils';
import {applabObjectFitImages} from '../applabObjectFitImages';

class ImageProperties extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -76,6 +78,12 @@ class ImageProperties extends React.Component {
handleChange={this.props.handleChange.bind(this, 'picture')}
/>
{iconColorPicker}
<EnumPropertyRow
desc={'fit image'}
initialValue={element.style.objectFit || 'fill'}
options={['fill','cover','contain','none']}
handleChange={this.props.handleChange.bind(this, 'objectFit')}
/>
<BooleanPropertyRow
desc={'hidden'}
initialValue={$(element).hasClass('design-mode-hidden')}
Expand Down Expand Up @@ -139,6 +147,17 @@ class ImageEvents extends React.Component {
}
}

function setObjectFitStyles(element, value) {
// NOTE: neither of these will be saved (we strip these out when we serialize
// and rely on our custom data-object-fit attribute during save/load)

// Set a style for modern browsers:
element.style.objectFit = value;

// Set a style that will be picked up by objectFitImages() for old browsers:
element.style.fontFamily = `'object-fit: ${value};'`;
applabObjectFitImages(element);
}

export default {
PropertyTab: ImageProperties,
Expand All @@ -151,6 +170,11 @@ export default {
element.setAttribute('src', '/blockly/media/1x1.gif');
element.setAttribute('data-canonical-image-url', '');

// New elements are created with 'contain', but the default value for
// existing (unadorned) images is 'fill' for compatibility reasons
element.setAttribute('data-object-fit', 'contain');
setObjectFitStyles(element, 'contain');

return element;
},
onDeserialize: function (element, updateProperty) {
Expand All @@ -161,5 +185,28 @@ export default {
element.setAttribute('src', '/blockly/media/1x1.gif');
element.setAttribute('data-canonical-image-url', '');
}
const objectFitValue = element.getAttribute('data-object-fit');
if (objectFitValue) {
setObjectFitStyles(element, objectFitValue);
}
},
onPropertyChange: function (element, name, value) {
switch (name) {
case 'objectFit':
element.setAttribute('data-object-fit', value);
setObjectFitStyles(element, value);
break;
default:
return false;
}
return true;
},
readProperty: function (element, name) {
switch (name) {
case 'objectFit':
return element.getAttribute('data-object-fit');
default:
throw `unknown property name ${name}`;
}
}
};
55 changes: 23 additions & 32 deletions apps/src/applab/designMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import logToCloud from '../logToCloud';
import {actions} from './redux/applab';
import * as screens from './redux/screens';
import {getStore} from '../redux';
import {applabObjectFitImages} from './applabObjectFitImages';

var designMode = {};
export default designMode;
Expand Down Expand Up @@ -318,38 +319,8 @@ designMode.updateProperty = function (element, name, value) {

if (ICON_PREFIX_REGEX.test(value)) {
element.src = assetPrefix.renderIconToString(value, element);
break;
}

element.src = assetPrefix.fixPath(value);
// do not resize if only the asset path has changed (e.g. on remix).
if (value !== originalValue) {
var resizeElement = function (width, height) {
element.style.width = width + 'px';
element.style.height = height + 'px';
if (gridUtils.isDraggableContainer(element.parentNode)) {
element.parentNode.style.width = width + 'px';
element.parentNode.style.height = height + 'px';
}
// Re-render properties
if (currentlyEditedElement === element) {
designMode.editElementProperties(element);
}
};
if (value === '') {
element.src = '/blockly/media/1x1.gif';
resizeElement(100, 100);
} else {
element.onload = function () {
// naturalWidth/Height aren't populated until image has loaded.
var left = parseFloat(element.style.left);
var top = parseFloat(element.style.top);
var dimensions = boundedResize(left, top, element.naturalWidth, element.naturalHeight, true);
resizeElement(dimensions.width, dimensions.height);
// only perform onload once
element.onload = null;
};
}
} else {
element.src = value === '' ? '/blockly/media/1x1.gif' : assetPrefix.fixPath(value);
}
break;
case 'hidden':
Expand Down Expand Up @@ -664,6 +635,25 @@ designMode.serializeToLevelHtml = function () {
});
designModeVizClone.children().children().each(function () {
elementUtils.removeIdPrefix(this);
if (this.nodeName === 'IMG') {
// Remove object-fit style and all styles and attributes used by the
// the object-fit-images polyfill for IE and replace the src attribute

// (We will rely on our own data-object-fit property for serialization)
this.style.objectFit = '';
this.style.backgroundPosition = '';
this.style.backgroundImage = '';
this.style.backgroundRepeat = '';
this.style.backgroundOrigin = '';
this.style.backgroundSize = '';
this.style.fontFamily = '';
this.removeAttribute('data-ofi-undefined');
const ofiSrc = this.getAttribute('data-ofi-src');
if (ofiSrc) {
this.src = ofiSrc;
this.removeAttribute('data-ofi-src');
}
}
});

// Remove the "data:img/png..." URI from icon images
Expand Down Expand Up @@ -936,6 +926,7 @@ function makeDraggable(jqueryElements) {

elm.css('position', 'static');
});
applabObjectFitImages();
}

/**
Expand Down
8 changes: 6 additions & 2 deletions apps/src/applab/setPropertyDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ var PROP_INFO = {
min: { friendlyName: 'min', internalName: 'min', type: 'number', defaultValue: '100' },
max: { friendlyName: 'max', internalName: 'max', type: 'number', defaultValue: '100' },
step: { friendlyName: 'step', internalName: 'step', type: 'number', defaultValue: '100' },
value: { friendlyName: 'value', internalName: 'value', type: 'uistring', defaultValue: '"text"' }
value: { friendlyName: 'value', internalName: 'value', type: 'uistring', defaultValue: '"text"' },
fit: { friendlyName: 'fit', internalName: 'objectFit', type: 'string', defaultValue: '"fill"' }
};

// When we don't know the element type, we display all possible friendly names
Expand Down Expand Up @@ -169,7 +170,8 @@ PROPERTIES[ElementType.IMAGE] = {
'pictureImage',
'picture', // Since this is an alias, it is not shown in the dropdown but is allowed as a value
'iconColor',
'hidden'
'hidden',
'fit'
]
};
PROPERTIES[ElementType.CANVAS] = {
Expand Down Expand Up @@ -371,6 +373,8 @@ function getPropertyValueDropdown(param2) {
return ['"red"', 'rgb(255,0,0)', 'rgb(255,0,0,0.5)', '"#FF0000"'];
case "text-align":
return ['"left"', '"right"', '"center"', '"justify"'];
case "fit":
return ['"fill"', '"cover"', '"contain"', '"none"'];
case "hidden":
case "checked":
case "readonly":
Expand Down
6 changes: 3 additions & 3 deletions apps/style/applab/skins/modern.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@
}
}

#designModeViz.appModern, .draggingParent {

#divApplab.notRunning, #designModeViz.appModern, .draggingParent {
img[data-canonical-image-url=''], canvas, .chart {
border: 5px solid #fff;
box-sizing: border-box;
Expand All @@ -71,7 +70,8 @@

img[data-canonical-image-url=''] {
display: block;
background: #bdc3c7 url("/blockly/media/applab/placeholder_img.png") center center no-repeat;
// Using !important so that object-fit-images polyfill (which creates inline background styles) doesn't override the placeholder
background: #bdc3c7 url("/blockly/media/applab/placeholder_img.png") center center no-repeat !important;
}

canvas {
Expand Down
7 changes: 3 additions & 4 deletions apps/test/integration/levelSolutions/applab/ec_design.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,9 @@ module.exports = {
assertPropertyRowExists(3, 'x position (px)', assert);
assertPropertyRowExists(4, 'y position (px)', assert);
assertPropertyRowExists(5, 'image Choose...', assert);
assertPropertyRowExists(6, 'hidden', assert);
assertPropertyRowExists(7, 'depth', assert);
assertPropertyRowExists(6, 'fit imagefillcovercontainnone', assert);
assertPropertyRowExists(7, 'hidden', assert);
assertPropertyRowExists(8, 'depth', assert);

// Make sure it's draggable
var manipulator = newImage.parent();
Expand Down Expand Up @@ -871,8 +872,6 @@ module.exports = {

assert(/1x1.gif$/.test(designImage.src), 'src became 1x1.gif');
assert.equal(designImage.getAttribute('data-canonical-image-url'), '');
assert.equal(designImage.style.width, '100px', 'width is reset');
assert.equal(designImage.style.height, '100px', 'height is reset');

Applab.onPuzzleComplete();
},
Expand Down
22 changes: 22 additions & 0 deletions apps/test/integration/levelSolutions/applab/ec_uicontrols.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ module.exports = {
testResult: TestResults.FREE_PLAY
},
},
{
description: "getProperty and setProperty for fit on images.",
editCode: true,
xml:
"image('idImage1', '');" +
"image('idImage2', '');" +
"image('idImage3', '');" +
"setProperty('idImage1', 'fit', 'cover');" +
"setProperty('idImage2', 'fit', getProperty('idImage1', 'fit'));",
runBeforeClick: function (assert) {
// add a completion on timeout since this is a freeplay level
tickWrapper.runOnAppTick(Applab, 2, function () {
assert(document.getElementById('idImage2').getAttribute('data-object-fit') === 'cover');
assert(document.getElementById('idImage3').getAttribute('data-object-fit') === 'contain');
Applab.onPuzzleComplete();
});
},
expected: {
result: true,
testResult: TestResults.FREE_PLAY
},
},
{
description: "button",
editCode: true,
Expand Down
4 changes: 4 additions & 0 deletions apps/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6556,6 +6556,10 @@ object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"

object-fit-images@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/object-fit-images/-/object-fit-images-3.2.3.tgz#4089f6d0070a3b5563d3c1ab6f1b28d61331f0ac"

object-is@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
Expand Down