Skip to content

Commit

Permalink
Merge pull request #19011 from code-dot-org/applab_image_fit_property
Browse files Browse the repository at this point in the history
Applab: add fit property for images
  • Loading branch information
cpirich committed Nov 10, 2017
2 parents 7b5d714 + 9fff057 commit 58e3c63
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 41 deletions.
1 change: 1 addition & 0 deletions apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,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)) {
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 @@ -322,38 +323,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 @@ -668,6 +639,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 @@ -940,6 +930,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

0 comments on commit 58e3c63

Please sign in to comment.