turtlestitch/widgets.js

3184 wiersze
84 KiB
JavaScript

/*
widgets.js
additional GUI elements for morphic.js
written by Jens Mönig
jens@moenig.org
Copyright (C) 2013 by Jens Mönig
This file is part of Snap!.
Snap! is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
prerequisites:
--------------
needs blocks.js and objects.js
I. hierarchy
-------------
the following tree lists all constructors hierarchically,
indentation indicating inheritance. Refer to this list to get a
contextual overview:
Morph*
AlignmentMorph
DialogBoxMorph
InputFieldMorph
TriggerMorph*
PushButtonMorph
ToggleButtonMorph
TabMorph
ToggleMorph
ToggleElementMorph
* from Morphic.js
II. toc
-------
the following list shows the order in which all constructors are
defined. Use this list to locate code in this document:
PushButtonMorph
ToggleButtonMorph
TabMorph
ToggleMorph
ToggleElementMorph
DialogBoxMorph
AlignmentMorph
InputFieldMorph
*/
// Global settings /////////////////////////////////////////////////////
/*global TriggerMorph, modules, Color, Point, BoxMorph, radians,
newCanvas, StringMorph, Morph, TextMorph, nop, detect, StringFieldMorph,
HTMLCanvasElement, fontHeight, SymbolMorph, localize, SpeechBubbleMorph,
ArrowMorph, MenuMorph, isString, isNil, SliderMorph, MorphicPreferences,
ScrollFrameMorph*/
modules.widgets = '2013-October-25';
var PushButtonMorph;
var ToggleButtonMorph;
var TabMorph;
var ToggleMorph;
var ToggleElementMorph;
var DialogBoxMorph;
var AlignmentMorph;
var InputFieldMorph;
// PushButtonMorph /////////////////////////////////////////////////////
// I am a Button with rounded corners and 3D-ish graphical effects
// PushButtonMorph inherits from TriggerMorph:
PushButtonMorph.prototype = new TriggerMorph();
PushButtonMorph.prototype.constructor = PushButtonMorph;
PushButtonMorph.uber = TriggerMorph.prototype;
// PushButtonMorph preferences settings:
PushButtonMorph.prototype.fontSize = 10;
PushButtonMorph.prototype.fontStyle = 'sans-serif';
PushButtonMorph.prototype.labelColor = new Color(0, 0, 0);
PushButtonMorph.prototype.labelShadowColor = new Color(255, 255, 255);
PushButtonMorph.prototype.labelShadowOffset = new Point(1, 1);
PushButtonMorph.prototype.color = new Color(220, 220, 220);
PushButtonMorph.prototype.pressColor = new Color(115, 180, 240);
PushButtonMorph.prototype.highlightColor
= PushButtonMorph.prototype.pressColor.lighter(50);
PushButtonMorph.prototype.outlineColor = new Color(30, 30, 30);
PushButtonMorph.prototype.outlineGradient = false;
PushButtonMorph.prototype.contrast = 60;
PushButtonMorph.prototype.edge = 2;
PushButtonMorph.prototype.corner = 5;
PushButtonMorph.prototype.outline = 1.00001;
PushButtonMorph.prototype.padding = 3;
// PushButtonMorph instance creation:
function PushButtonMorph(
target,
action,
labelString,
environment,
hint,
template
) {
this.init(
target,
action,
labelString,
environment,
hint,
template
);
}
PushButtonMorph.prototype.init = function (
target,
action,
labelString,
environment,
hint,
template
) {
// additional properties:
this.is3D = false; // for "flat" design exceptions
this.target = target || null;
this.action = action || null;
this.environment = environment || null;
this.labelString = labelString || null;
this.label = null;
this.labelMinExtent = new Point(0, 0);
this.hint = hint || null;
this.template = template || null; // for pre-computed backbrounds
// if a template is specified, its background images are used as cache
// initialize inherited properties:
TriggerMorph.uber.init.call(this);
// override inherited properites:
this.color = PushButtonMorph.prototype.color;
this.drawNew();
this.fixLayout();
};
// PushButtonMorph layout:
PushButtonMorph.prototype.fixLayout = function () {
// make sure I at least encompass my label
if (this.label !== null) {
var padding = this.padding * 2 + this.outline * 2 + this.edge * 2;
this.setExtent(new Point(
Math.max(this.label.width(), this.labelMinExtent.x) + padding,
Math.max(this.label instanceof StringMorph ?
this.label.rawHeight() :
this.label.height(), this.labelMinExtent.y) + padding
));
this.label.setCenter(this.center());
}
};
// PushButtonMorph events
PushButtonMorph.prototype.mouseDownLeft = function () {
PushButtonMorph.uber.mouseDownLeft.call(this);
if (this.label) {
this.label.setCenter(this.center().add(1));
}
};
PushButtonMorph.prototype.mouseClickLeft = function () {
PushButtonMorph.uber.mouseClickLeft.call(this);
if (this.label) {
this.label.setCenter(this.center());
}
};
PushButtonMorph.prototype.mouseLeave = function () {
PushButtonMorph.uber.mouseLeave.call(this);
if (this.label) {
this.label.setCenter(this.center());
}
};
// PushButtonMorph drawing:
PushButtonMorph.prototype.outlinePath = BoxMorph.prototype.outlinePath;
PushButtonMorph.prototype.drawOutline = function (context) {
var outlineStyle,
isFlat = MorphicPreferences.isFlat && !this.is3D;
if (!this.outline || isFlat) {return null; }
if (this.outlineGradient) {
outlineStyle = context.createLinearGradient(
0,
0,
0,
this.height()
);
outlineStyle.addColorStop(1, 'white');
outlineStyle.addColorStop(0, this.outlineColor.darker().toString());
} else {
outlineStyle = this.outlineColor.toString();
}
context.fillStyle = outlineStyle;
context.beginPath();
this.outlinePath(
context,
isFlat ? 0 : this.corner,
0
);
context.closePath();
context.fill();
};
PushButtonMorph.prototype.drawBackground = function (context, color) {
var isFlat = MorphicPreferences.isFlat && !this.is3D;
context.fillStyle = color.toString();
context.beginPath();
this.outlinePath(
context,
isFlat ? 0 : Math.max(this.corner - this.outline, 0),
this.outline
);
context.closePath();
context.fill();
context.lineWidth = this.outline;
};
PushButtonMorph.prototype.drawEdges = function (
context,
color,
topColor,
bottomColor
) {
if (MorphicPreferences.isFlat && !this.is3D) {return; }
var minInset = Math.max(this.corner, this.outline + this.edge),
w = this.width(),
h = this.height(),
gradient;
// top:
gradient = context.createLinearGradient(
0,
this.outline,
0,
this.outline + this.edge
);
gradient.addColorStop(0, topColor.toString());
gradient.addColorStop(1, color.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = this.edge;
context.beginPath();
context.moveTo(minInset, this.outline + this.edge / 2);
context.lineTo(w - minInset, this.outline + this.edge / 2);
context.stroke();
// top-left corner:
gradient = context.createRadialGradient(
this.corner,
this.corner,
Math.max(this.corner - this.outline - this.edge, 0),
this.corner,
this.corner,
Math.max(this.corner - this.outline, 0)
);
gradient.addColorStop(1, topColor.toString());
gradient.addColorStop(0, color.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = this.edge;
context.beginPath();
context.arc(
this.corner,
this.corner,
Math.max(this.corner - this.outline - this.edge / 2, 0),
radians(180),
radians(270),
false
);
context.stroke();
// left:
gradient = context.createLinearGradient(
this.outline,
0,
this.outline + this.edge,
0
);
gradient.addColorStop(0, topColor.toString());
gradient.addColorStop(1, color.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = this.edge;
context.beginPath();
context.moveTo(this.outline + this.edge / 2, minInset);
context.lineTo(this.outline + this.edge / 2, h - minInset);
context.stroke();
// bottom:
gradient = context.createLinearGradient(
0,
h - this.outline,
0,
h - this.outline - this.edge
);
gradient.addColorStop(0, bottomColor.toString());
gradient.addColorStop(1, color.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = this.edge;
context.beginPath();
context.moveTo(minInset, h - this.outline - this.edge / 2);
context.lineTo(w - minInset, h - this.outline - this.edge / 2);
context.stroke();
// bottom-right corner:
gradient = context.createRadialGradient(
w - this.corner,
h - this.corner,
Math.max(this.corner - this.outline - this.edge, 0),
w - this.corner,
h - this.corner,
Math.max(this.corner - this.outline, 0)
);
gradient.addColorStop(1, bottomColor.toString());
gradient.addColorStop(0, color.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = this.edge;
context.beginPath();
context.arc(
w - this.corner,
h - this.corner,
Math.max(this.corner - this.outline - this.edge / 2, 0),
radians(0),
radians(90),
false
);
context.stroke();
// right:
gradient = context.createLinearGradient(
w - this.outline,
0,
w - this.outline - this.edge,
0
);
gradient.addColorStop(0, bottomColor.toString());
gradient.addColorStop(1, color.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = this.edge;
context.beginPath();
context.moveTo(w - this.outline - this.edge / 2, minInset);
context.lineTo(w - this.outline - this.edge / 2, h - minInset);
context.stroke();
};
PushButtonMorph.prototype.createBackgrounds = function () {
var context,
ext = this.extent();
if (this.template) { // take the backgrounds images from the template
this.image = this.template.image;
this.normalImage = this.template.normalImage;
this.highlightImage = this.template.highlightImage;
this.pressImage = this.template.pressImage;
return null;
}
this.normalImage = newCanvas(ext);
context = this.normalImage.getContext('2d');
this.drawOutline(context);
this.drawBackground(context, this.color);
this.drawEdges(
context,
this.color,
this.color.lighter(this.contrast),
this.color.darker(this.contrast)
);
this.highlightImage = newCanvas(ext);
context = this.highlightImage.getContext('2d');
this.drawOutline(context);
this.drawBackground(context, this.highlightColor);
this.drawEdges(
context,
this.highlightColor,
this.highlightColor.lighter(this.contrast),
this.highlightColor.darker(this.contrast)
);
this.pressImage = newCanvas(ext);
context = this.pressImage.getContext('2d');
this.drawOutline(context);
this.drawBackground(context, this.pressColor);
this.drawEdges(
context,
this.pressColor,
this.pressColor.darker(this.contrast),
this.pressColor.lighter(this.contrast)
);
this.image = this.normalImage;
};
PushButtonMorph.prototype.createLabel = function () {
var shading = !MorphicPreferences.isFlat || this.is3D;
if (this.label !== null) {
this.label.destroy();
}
if (this.labelString instanceof SymbolMorph) {
this.label = this.labelString.fullCopy();
if (shading) {
this.label.shadowOffset = this.labelShadowOffset;
this.label.shadowColor = this.labelShadowColor;
}
this.label.color = this.labelColor;
this.label.drawNew();
} else {
this.label = new StringMorph(
localize(this.labelString),
this.fontSize,
this.fontStyle,
true,
false,
false,
shading ? this.labelShadowOffset : null,
this.labelShadowColor,
this.labelColor
);
}
this.add(this.label);
};
// ToggleButtonMorph ///////////////////////////////////////////////////////
/*
I am a two-state PushButton. When my state is "true" I keep my "pressed"
background color. I can also be set to not auto-layout my bounds, in
which case my label will left-align.
*/
// ToggleButtonMorph inherits from PushButtonMorph:
ToggleButtonMorph.prototype = new PushButtonMorph();
ToggleButtonMorph.prototype.constructor = ToggleButtonMorph;
ToggleButtonMorph.uber = PushButtonMorph.prototype;
// ToggleButton settings
ToggleButtonMorph.prototype.contrast = 30;
// ToggleButtonMorph instance creation:
function ToggleButtonMorph(
colors, // color overrides, <array>: [normal, highlight, pressed]
target,
action, // a toggle function
labelString,
query, // predicate/selector
environment,
hint,
template, // optional, for cached background images
minWidth, // <num> optional, if specified label will left-align
hasPreview, // <bool> show press color on left edge (e.g. category)
isPicture // treat label as picture, i.e. don't apply typography
) {
this.init(
colors,
target,
action,
labelString,
query,
environment,
hint,
template,
minWidth,
hasPreview,
isPicture
);
}
ToggleButtonMorph.prototype.init = function (
colors,
target,
action,
labelString,
query,
environment,
hint,
template,
minWidth,
hasPreview,
isPicture
) {
// additional properties:
this.state = false;
this.query = query || function () {return true; };
this.minWidth = minWidth || null;
this.hasPreview = hasPreview || false;
this.isPicture = isPicture || false;
this.trueStateLabel = null;
// initialize inherited properties:
ToggleButtonMorph.uber.init.call(
this,
target,
action,
labelString,
environment,
hint,
template
);
// override default colors if others are specified
if (colors) {
this.color = colors[0];
this.highlightColor = colors[1];
this.pressColor = colors[2];
}
this.refresh();
this.drawNew();
};
// ToggleButtonMorph events
ToggleButtonMorph.prototype.mouseEnter = function () {
if (!this.state) {
this.image = this.highlightImage;
this.changed();
}
if (this.hint) {
this.bubbleHelp(this.hint);
}
};
ToggleButtonMorph.prototype.mouseLeave = function () {
if (!this.state) {
this.image = this.normalImage;
this.changed();
}
if (this.hint) {
this.world().hand.destroyTemporaries();
}
};
ToggleButtonMorph.prototype.mouseDownLeft = function () {
if (!this.state) {
this.image = this.pressImage;
this.changed();
}
};
ToggleButtonMorph.prototype.mouseClickLeft = function () {
if (!this.state) {
this.image = this.highlightImage;
this.changed();
}
this.trigger(); // allow me to be triggered again to force-update others
};
// ToggleButtonMorph action
ToggleButtonMorph.prototype.trigger = function () {
ToggleButtonMorph.uber.trigger.call(this);
this.refresh();
};
ToggleButtonMorph.prototype.refresh = function () {
/*
if query is a function:
execute the query with target as environment (can be null)
for lambdafied (inline) actions
else if query is a String:
treat it as function property of target and execute it
for selector-like queries
*/
if (typeof this.query === 'function') {
this.state = this.query.call(this.target);
} else { // assume it's a String
this.state = this.target[this.query]();
}
if (this.state) {
this.image = this.pressImage;
if (this.trueStateLabel) {
this.label.hide();
this.trueStateLabel.show();
}
} else {
this.image = this.normalImage;
if (this.trueStateLabel) {
this.label.show();
this.trueStateLabel.hide();
}
}
this.changed();
};
// ToggleButtonMorph layout:
ToggleButtonMorph.prototype.fixLayout = function () {
if (this.label !== null) {
var lw = Math.max(this.label.width(), this.labelMinExtent.x),
padding = this.padding * 2 + this.outline * 2 + this.edge * 2;
this.setExtent(new Point(
(this.minWidth ?
Math.max(this.minWidth, lw) + padding
: lw + padding),
Math.max(this.label instanceof StringMorph ?
this.label.rawHeight() :
this.label.height(), this.labelMinExtent.y) + padding
));
this.label.setCenter(this.center());
if (this.trueStateLabel) {
this.trueStateLabel.setCenter(this.center());
}
if (this.minWidth) { // left-align along my corner
this.label.setLeft(
this.left()
+ this.outline
+ this.edge
+ this.corner
+ this.padding
);
}
}
};
// ToggleButtonMorph drawing
ToggleButtonMorph.prototype.createBackgrounds = function () {
/*
basically the same as inherited from PushButtonMorph, except for
not inverting the pressImage 3D-ish border (because it stays that way),
and optionally coloring the left edge in the press-color, previewing
the selection color (e.g. in the case of Scratch palette-category
selector. the latter is done in the drawEdges() method.
*/
var context,
ext = this.extent();
if (this.template) { // take the backgrounds images from the template
this.image = this.template.image;
this.normalImage = this.template.normalImage;
this.highlightImage = this.template.highlightImage;
this.pressImage = this.template.pressImage;
return null;
}
this.normalImage = newCanvas(ext);
context = this.normalImage.getContext('2d');
this.drawOutline(context);
this.drawBackground(context, this.color);
this.drawEdges(
context,
this.color,
this.color.lighter(this.contrast),
this.color.darker(this.contrast)
);
this.highlightImage = newCanvas(ext);
context = this.highlightImage.getContext('2d');
this.drawOutline(context);
this.drawBackground(context, this.highlightColor);
this.drawEdges(
context,
this.highlightColor,
this.highlightColor.lighter(this.contrast),
this.highlightColor.darker(this.contrast)
);
// note: don't invert the 3D-ish edges for pressedImage, because
// it will stay that way, and should not look inverted (or should it?)
this.pressImage = newCanvas(ext);
context = this.pressImage.getContext('2d');
this.drawOutline(context);
this.drawBackground(context, this.pressColor);
this.drawEdges(
context,
this.pressColor,
this.pressColor.lighter(40),
this.pressColor.darker(40)
);
this.image = this.normalImage;
};
ToggleButtonMorph.prototype.drawEdges = function (
context,
color,
topColor,
bottomColor
) {
var gradient;
ToggleButtonMorph.uber.drawEdges.call(
this,
context,
color,
topColor,
bottomColor
);
if (this.hasPreview) { // indicate the possible selection color
if (MorphicPreferences.isFlat && !this.is3D) {
context.fillStyle = this.pressColor.toString();
context.fillRect(
this.outline,
this.outline,
this.corner,
this.height() - this.outline * 2
);
return;
}
gradient = context.createLinearGradient(
0,
0,
this.corner,
0
);
gradient.addColorStop(0, this.pressColor.lighter(40).toString());
gradient.addColorStop(1, this.pressColor.darker(40).toString());
context.fillStyle = gradient; // this.pressColor.toString();
context.beginPath();
this.previewPath(
context,
Math.max(this.corner - this.outline, 0),
this.outline
);
context.closePath();
context.fill();
}
};
ToggleButtonMorph.prototype.previewPath = function (context, radius, inset) {
var offset = radius + inset,
h = this.height();
// top left:
context.arc(
offset,
offset,
radius,
radians(-180),
radians(-90),
false
);
// bottom left:
context.arc(
offset,
h - offset,
radius,
radians(90),
radians(180),
false
);
};
ToggleButtonMorph.prototype.createLabel = function () {
var shading = !MorphicPreferences.isFlat || this.is3D,
none = new Point();
if (this.label !== null) {
this.label.destroy();
}
if (this.trueStateLabel !== null) {
this.trueStateLabel.destroy();
}
if (this.labelString instanceof Array && this.labelString.length === 2) {
if (this.labelString[0] instanceof SymbolMorph) {
this.label = this.labelString[0].fullCopy();
this.trueStateLabel = this.labelString[1].fullCopy();
if (!this.isPicture) {
this.label.shadowOffset = shading ?
this.labelShadowOffset : none;
this.label.shadowColor = this.labelShadowColor;
this.label.color = this.labelColor;
this.label.drawNew();
this.trueStateLabel.shadowOffset = shading ?
this.labelShadowOffset : none;
this.trueStateLabel.shadowColor = this.labelShadowColor;
this.trueStateLabel.color = this.labelColor;
this.trueStateLabel.drawNew();
}
} else if (this.labelString[0] instanceof Morph) {
this.label = this.labelString[0].fullCopy();
this.trueStateLabel = this.labelString[1].fullCopy();
} else {
this.label = new StringMorph(
localize(this.labelString[0]),
this.fontSize,
this.fontStyle,
true,
false,
false,
shading ? this.labelShadowOffset : null,
this.labelShadowColor,
this.labelColor
);
this.trueStateLabel = new StringMorph(
localize(this.labelString[1]),
this.fontSize,
this.fontStyle,
true,
false,
false,
shading ? this.labelShadowOffset : null,
this.labelShadowColor,
this.labelColor
);
}
} else {
if (this.labelString instanceof SymbolMorph) {
this.label = this.labelString.fullCopy();
if (!this.isPicture) {
this.label.shadowOffset = shading ?
this.labelShadowOffset : none;
this.label.shadowColor = this.labelShadowColor;
this.label.color = this.labelColor;
this.label.drawNew();
}
} else if (this.labelString instanceof Morph) {
this.label = this.labelString.fullCopy();
} else {
this.label = new StringMorph(
localize(this.labelString),
this.fontSize,
this.fontStyle,
true,
false,
false,
shading ? this.labelShadowOffset : none,
this.labelShadowColor,
this.labelColor
);
}
}
this.add(this.label);
if (this.trueStateLabel) {
this.add(this.trueStateLabel);
}
};
// ToggleButtonMorph hiding and showing:
/*
override the inherited behavior to recursively hide/show all
children, so that my instances get restored correctly when
hiding/showing my parent.
*/
ToggleButtonMorph.prototype.hide = function () {
this.isVisible = false;
this.changed();
};
ToggleButtonMorph.prototype.show = function () {
this.isVisible = true;
this.changed();
};
// TabMorph ///////////////////////////////////////////////////////
// TabMorph inherits from ToggleButtonMorph:
TabMorph.prototype = new ToggleButtonMorph();
TabMorph.prototype.constructor = TabMorph;
TabMorph.uber = ToggleButtonMorph.prototype;
// TabMorph instance creation:
function TabMorph(
colors, // color overrides, <array>: [normal, highlight, pressed]
target,
action, // a toggle function
labelString,
query, // predicate/selector
environment,
hint
) {
this.init(
colors,
target,
action,
labelString,
query,
environment,
hint
);
}
// TabMorph layout:
TabMorph.prototype.fixLayout = function () {
if (this.label !== null) {
this.setExtent(new Point(
this.label.width()
+ this.padding * 2
+ this.corner * 3
+ this.edge * 2,
(this.label instanceof StringMorph ?
this.label.rawHeight() : this.label.height())
+ this.padding * 2
+ this.edge
));
this.label.setCenter(this.center());
}
};
// TabMorph action:
TabMorph.prototype.refresh = function () {
if (this.state) { // bring to front
if (this.parent) {
this.parent.add(this);
}
}
TabMorph.uber.refresh.call(this);
};
// TabMorph drawing:
TabMorph.prototype.drawBackground = function (context, color) {
var w = this.width(),
h = this.height(),
c = this.corner;
context.fillStyle = color.toString();
context.beginPath();
context.moveTo(0, h);
context.bezierCurveTo(c, h, c, 0, c * 2, 0);
context.lineTo(w - c * 2, 0);
context.bezierCurveTo(w - c, 0, w - c, h, w, h);
context.closePath();
context.fill();
};
TabMorph.prototype.drawOutline = function () {
nop();
};
TabMorph.prototype.drawEdges = function (
context,
color,
topColor,
bottomColor
) {
if (MorphicPreferences.isFlat && !this.is3D) {return; }
var w = this.width(),
h = this.height(),
c = this.corner,
e = this.edge,
eh = e / 2,
gradient;
nop(color); // argument not needed here
gradient = context.createLinearGradient(0, 0, w, 0);
gradient.addColorStop(0, topColor.toString());
gradient.addColorStop(1, bottomColor.toString());
context.strokeStyle = gradient;
context.lineCap = 'round';
context.lineWidth = e;
context.beginPath();
context.moveTo(0, h + eh);
context.bezierCurveTo(c, h, c, 0, c * 2, eh);
context.lineTo(w - c * 2, eh);
context.bezierCurveTo(w - c, 0, w - c, h, w, h + eh);
context.stroke();
};
// ToggleMorph ///////////////////////////////////////////////////////
/*
I am a PushButton which toggles a check mark ( becoming check box)
or a bullet (becoming a radio button). I can have both or either an
additional label and an additional pictogram, whereas the pictogram
can be either an instance of (any) Morph, in which case the pictogram
will be an interactive toggle itself or a Canvas, in which case it
is just going to be a picture.
*/
// ToggleMorph inherits from PushButtonMorph:
ToggleMorph.prototype = new PushButtonMorph();
ToggleMorph.prototype.constructor = ToggleMorph;
ToggleMorph.uber = PushButtonMorph.prototype;
// ToggleMorph instance creation:
function ToggleMorph(
style, // 'checkbox' or 'radiobutton'
target,
action, // a toggle function
labelString,
query, // predicate/selector
environment,
hint,
template,
element, // optional Morph or Canvas to display
builder // method which constructs the element (only for Morphs)
) {
this.init(
style,
target,
action,
labelString,
query,
environment,
hint,
template,
element,
builder
);
}
ToggleMorph.prototype.init = function (
style,
target,
action,
labelString,
query,
environment,
hint,
template,
element,
builder
) {
// additional properties:
this.padding = 1;
style = style || 'checkbox';
this.corner = (style === 'checkbox' ?
0 : fontHeight(this.fontSize) / 2 + this.outline + this.padding);
this.state = false;
this.query = query || function () {return true; };
this.tick = null;
this.captionString = labelString || null;
this.labelAlignment = 'right';
this.element = element || null;
this.builder = builder || null;
this.toggleElement = null;
// initialize inherited properties:
ToggleMorph.uber.init.call(
this,
target,
action,
(style === 'checkbox' ? '\u2713' : '\u25CF'),
environment,
hint,
template
);
this.refresh();
this.drawNew();
};
// ToggleMorph layout:
ToggleMorph.prototype.fixLayout = function () {
var padding = this.padding * 2 + this.outline * 2,
y;
if (this.tick !== null) {
this.silentSetHeight(this.tick.rawHeight() + padding);
this.silentSetWidth(this.tick.width() + padding);
this.setExtent(new Point(
Math.max(this.width(), this.height()),
Math.max(this.width(), this.height())
));
this.tick.setCenter(this.center());
}
if (this.state) {
this.tick.show();
} else {
this.tick.hide();
}
if (this.toggleElement && (this.labelAlignment === 'right')) {
y = this.top() + (this.height() - this.toggleElement.height()) / 2;
this.toggleElement.setPosition(new Point(
this.right() + padding,
y
));
}
if (this.label !== null) {
y = this.top() + (this.height() - this.label.height()) / 2;
if (this.labelAlignment === 'right') {
this.label.setPosition(new Point(
this.toggleElement ?
this.toggleElement instanceof ToggleElementMorph ?
this.toggleElement.right()
: this.toggleElement.right() + padding
: this.right() + padding,
y
));
} else {
this.label.setPosition(new Point(
this.left() - this.label.width() - padding,
y
));
}
}
};
ToggleMorph.prototype.createLabel = function () {
var shading = !MorphicPreferences.isFlat || this.is3D;
if (this.label === null) {
if (this.captionString) {
this.label = new TextMorph(
localize(this.captionString),
this.fontSize,
this.fontStyle,
true
);
this.add(this.label);
}
}
if (this.tick === null) {
this.tick = new StringMorph(
localize(this.labelString),
this.fontSize,
this.fontStyle,
true,
false,
false,
shading ? new Point(1, 1) : null,
new Color(240, 240, 240)
);
this.add(this.tick);
}
if (this.toggleElement === null) {
if (this.element) {
if (this.element instanceof Morph) {
this.toggleElement = new ToggleElementMorph(
this.target,
this.action,
this.element,
this.query,
this.environment,
this.hint,
this.builder
);
} else if (this.element instanceof HTMLCanvasElement) {
this.toggleElement = new Morph();
this.toggleElement.silentSetExtent(new Point(
this.element.width,
this.element.height
));
this.toggleElement.image = this.element;
}
this.add(this.toggleElement);
}
}
};
// ToggleMorph action:
ToggleMorph.prototype.trigger = function () {
ToggleMorph.uber.trigger.call(this);
this.refresh();
};
ToggleMorph.prototype.refresh = function () {
/*
if query is a function:
execute the query with target as environment (can be null)
for lambdafied (inline) actions
else if query is a String:
treat it as function property of target and execute it
for selector-like queries
*/
if (typeof this.query === 'function') {
this.state = this.query.call(this.target);
} else { // assume it's a String
this.state = this.target[this.query]();
}
if (this.state) {
this.tick.show();
} else {
this.tick.hide();
}
if (this.toggleElement && this.toggleElement.refresh) {
this.toggleElement.refresh();
}
};
// ToggleMorph events
ToggleMorph.prototype.mouseDownLeft = function () {
PushButtonMorph.uber.mouseDownLeft.call(this);
if (this.tick) {
this.tick.setCenter(this.center().add(1));
}
};
ToggleMorph.prototype.mouseClickLeft = function () {
PushButtonMorph.uber.mouseClickLeft.call(this);
if (this.tick) {
this.tick.setCenter(this.center());
}
};
ToggleMorph.prototype.mouseLeave = function () {
PushButtonMorph.uber.mouseLeave.call(this);
if (this.tick) {
this.tick.setCenter(this.center());
}
};
// ToggleMorph hiding and showing:
/*
override the inherited behavior to recursively hide/show all
children, so that my instances get restored correctly when
hiding/showing my parent.
*/
ToggleMorph.prototype.hide = ToggleButtonMorph.prototype.hide;
ToggleMorph.prototype.show = ToggleButtonMorph.prototype.show;
// ToggleElementMorph /////////////////////////////////////////////////////
/*
I am a picture of a Morph ("element") which acts as a toggle button.
I am different from ToggleButton in that I neither create a label nor
draw button outlines. Instead I display my element morph in specified
contrasts of a given color, symbolizing whether it is selected or not
*/
// ToggleElementMorph inherits from TriggerMorph:
ToggleElementMorph.prototype = new TriggerMorph();
ToggleElementMorph.prototype.constructor = ToggleElementMorph;
ToggleElementMorph.uber = TriggerMorph.prototype;
// ToggleElementMorph preferences settings
ToggleElementMorph.prototype.contrast = 50;
ToggleElementMorph.prototype.shadowOffset = new Point(2, 2);
ToggleElementMorph.prototype.shadowAlpha = 0.6;
ToggleElementMorph.prototype.fontSize = 10; // only for (optional) labels
ToggleElementMorph.prototype.inactiveColor = new Color(180, 180, 180);
// ToggleElementMorph instance creation:
function ToggleElementMorph(
target,
action,
element,
query,
environment,
hint,
builder,
labelString
) {
this.init(
target,
action,
element,
query,
environment,
hint,
builder,
labelString
);
}
ToggleElementMorph.prototype.init = function (
target,
action,
element, // mandatory
query,
environment,
hint,
builder, // optional function name that rebuilds the element
labelString
) {
// additional properties:
this.target = target || null;
this.action = action || null;
this.element = element;
this.query = query || function () {return true; };
this.environment = environment || null;
this.hint = hint || null;
this.builder = builder || 'nop';
this.captionString = labelString || null;
this.labelAlignment = 'right';
this.state = false;
// initialize inherited properties:
TriggerMorph.uber.init.call(this);
// override inherited properties:
this.color = element.color;
this.createLabel();
};
// ToggleElementMorph drawing:
ToggleElementMorph.prototype.createBackgrounds = function () {
var shading = !MorphicPreferences.isFlat || this.is3D;
this.color = this.element.color;
this.element.removeShadow();
this.element[this.builder]();
if (shading) {
this.element.addShadow(this.shadowOffset, this.shadowAlpha);
}
this.silentSetExtent(this.element.fullBounds().extent()); // w/ shadow
this.pressImage = this.element.fullImage();
this.element.removeShadow();
this.element.setColor(this.inactiveColor);
this.element[this.builder](this.contrast);
if (shading) {
this.element.addShadow(this.shadowOffset, 0);
}
this.normalImage = this.element.fullImage();
this.element.removeShadow();
this.element.setColor(this.color.lighter(this.contrast));
this.element[this.builder](this.contrast);
if (shading) {
this.element.addShadow(this.shadowOffset, this.shadowAlpha);
}
this.highlightImage = this.element.fullImage();
this.element.removeShadow();
this.element.setColor(this.color);
this.element[this.builder]();
this.image = this.normalImage;
};
ToggleElementMorph.prototype.setColor = function (aColor) {
this.element.setColor(aColor);
this.createBackgrounds();
this.refresh();
};
// ToggleElementMorph layout:
ToggleElementMorph.prototype.createLabel = function () {
var y;
if (this.captionString) {
this.label = new StringMorph(
this.captionString,
this.fontSize,
this.fontStyle,
true
);
this.add(this.label);
y = this.top() + (this.height() - this.label.height()) / 2;
if (this.labelAlignment === 'right') {
this.label.setPosition(new Point(
this.right(),
y
));
} else {
this.label.setPosition(new Point(
this.left() - this.label.width(),
y
));
}
}
};
// ToggleElementMorph action
ToggleElementMorph.prototype.trigger
= ToggleButtonMorph.prototype.trigger;
ToggleElementMorph.prototype.refresh
= ToggleButtonMorph.prototype.refresh;
// ToggleElementMorph events
ToggleElementMorph.prototype.mouseEnter
= ToggleButtonMorph.prototype.mouseEnter;
ToggleElementMorph.prototype.mouseLeave
= ToggleButtonMorph.prototype.mouseLeave;
ToggleElementMorph.prototype.mouseDownLeft
= ToggleButtonMorph.prototype.mouseDownLeft;
ToggleElementMorph.prototype.mouseClickLeft
= ToggleButtonMorph.prototype.mouseClickLeft;
// DialogBoxMorph /////////////////////////////////////////////////////
/*
I am a DialogBox frame.
Note:
-----
my key property keeps track of my purpose to prevent multiple instances
on the same or similar objects
*/
// DialogBoxMorph inherits from Morph:
DialogBoxMorph.prototype = new Morph();
DialogBoxMorph.prototype.constructor = DialogBoxMorph;
DialogBoxMorph.uber = Morph.prototype;
// DialogBoxMorph preferences settings:
DialogBoxMorph.prototype.fontSize = 12;
DialogBoxMorph.prototype.titleFontSize = 14;
DialogBoxMorph.prototype.fontStyle = 'sans-serif';
DialogBoxMorph.prototype.color = PushButtonMorph.prototype.color;
DialogBoxMorph.prototype.titleTextColor = new Color(255, 255, 255);
DialogBoxMorph.prototype.titleBarColor
= PushButtonMorph.prototype.pressColor;
DialogBoxMorph.prototype.contrast = 40;
DialogBoxMorph.prototype.corner = 12;
DialogBoxMorph.prototype.padding = 14;
DialogBoxMorph.prototype.titlePadding = 6;
DialogBoxMorph.prototype.buttonContrast = 50;
DialogBoxMorph.prototype.buttonFontSize = 12;
DialogBoxMorph.prototype.buttonCorner = 12;
DialogBoxMorph.prototype.buttonEdge = 6;
DialogBoxMorph.prototype.buttonPadding = 0;
DialogBoxMorph.prototype.buttonOutline = 3;
DialogBoxMorph.prototype.buttonOutlineColor
= PushButtonMorph.prototype.color;
DialogBoxMorph.prototype.buttonOutlineGradient = true;
DialogBoxMorph.prototype.instances = {}; // prevent multiple instances
// DialogBoxMorph instance creation:
function DialogBoxMorph(target, action, environment) {
this.init(target, action, environment);
}
DialogBoxMorph.prototype.init = function (target, action, environment) {
// additional properties:
this.is3D = false; // for "flat" design exceptions
this.target = target || null;
this.action = action || null;
this.environment = environment || null;
this.key = null; // keep track of my purpose to prevent mulitple instances
this.labelString = null;
this.label = null;
this.head = null;
this.body = null;
this.buttons = null;
// initialize inherited properties:
DialogBoxMorph.uber.init.call(this);
// override inherited properites:
this.isDraggable = true;
this.color = PushButtonMorph.prototype.color;
this.createLabel();
this.createButtons();
this.setExtent(new Point(300, 150));
};
// DialogBoxMorph ops
DialogBoxMorph.prototype.inform = function (
title,
textString,
world,
pic
) {
var txt = new TextMorph(
textString,
this.fontSize,
this.fontStyle,
true,
false,
'center',
null,
null,
MorphicPreferences.isFlat ? null : new Point(1, 1),
new Color(255, 255, 255)
);
if (!this.key) {
this.key = 'inform' + title + textString;
}
this.labelString = title;
this.createLabel();
if (pic) {this.setPicture(pic); }
if (textString) {
this.addBody(txt);
}
this.addButton('ok', 'OK');
this.drawNew();
this.fixLayout();
this.popUp(world);
};
DialogBoxMorph.prototype.askYesNo = function (
title,
textString,
world,
pic
) {
var txt = new TextMorph(
textString,
this.fontSize,
this.fontStyle,
true,
false,
'center',
null,
null,
MorphicPreferences.isFlat ? null : new Point(1, 1),
new Color(255, 255, 255)
);
if (!this.key) {
this.key = 'decide' + title + textString;
}
this.labelString = title;
this.createLabel();
if (pic) {this.setPicture(pic); }
this.addBody(txt);
this.addButton('ok', 'Yes');
this.addButton('cancel', 'No');
this.fixLayout();
this.drawNew();
this.fixLayout();
this.popUp(world);
};
DialogBoxMorph.prototype.prompt = function (
title,
defaultString,
world,
pic,
choices, // optional dictionary for drop-down of choices
isReadOnly, // optional when using choices
isNumeric, // optional
sliderMin, // optional for numeric sliders
sliderMax, // optional for numeric sliders
sliderAction // optional single-arg function for numeric slider
) {
var sld,
head,
txt = new InputFieldMorph(
defaultString,
isNumeric || false, // numeric?
choices || null, // drop-down dict, optional
choices ? isReadOnly || false : false
);
txt.setWidth(250);
if (isNumeric) {
if (pic) {
head = new AlignmentMorph('column', this.padding);
pic.setPosition(head.position());
head.add(pic);
}
if (!isNil(sliderMin) && !isNil(sliderMax)) {
sld = new SliderMorph(
sliderMin * 100,
sliderMax * 100,
parseFloat(defaultString) * 100,
(sliderMax - sliderMin) / 10 * 100,
'horizontal'
);
sld.alpha = 1;
sld.color = this.color.lighter(50);
sld.setHeight(txt.height() * 0.7);
sld.setWidth(txt.width());
sld.action = function (num) {
if (sliderAction) {
sliderAction(num / 100);
}
txt.setContents(num / 100);
txt.edit();
};
if (!head) {
head = new AlignmentMorph('column', this.padding);
}
head.add(sld);
}
if (head) {
head.fixLayout();
this.setPicture(head);
head.fixLayout();
}
} else {
if (pic) {this.setPicture(pic); }
}
this.reactToChoice = function (inp) {
if (sld) {
sld.value = inp * 100;
sld.drawNew();
sld.changed();
}
if (sliderAction) {
sliderAction(inp);
}
};
txt.reactToKeystroke = function () {
var inp = txt.getValue();
if (sld) {
inp = Math.max(inp, sliderMin);
sld.value = inp * 100;
sld.drawNew();
sld.changed();
}
if (sliderAction) {
sliderAction(inp);
}
};
this.labelString = title;
this.createLabel();
if (!this.key) {
this.key = 'prompt' + title + defaultString;
}
this.addBody(txt);
txt.drawNew();
this.addButton('ok', 'OK');
this.addButton('cancel', 'Cancel');
this.fixLayout();
this.drawNew();
this.fixLayout();
this.popUp(world);
};
DialogBoxMorph.prototype.promptCode = function (
title,
defaultString,
world,
pic,
instructions
) {
var frame = new ScrollFrameMorph(),
text = new TextMorph(defaultString || ''),
bdy = new AlignmentMorph('column', this.padding),
size = pic ? Math.max(pic.width, 400) : 400;
this.getInput = function () {
return text.text;
};
function remarkText(string) {
return new TextMorph(
string,
10,
null, // style
false, // bold
null, // italic
null, // alignment
null, // width
null, // font name
MorphicPreferences.isFlat ? null : new Point(1, 1),
new Color(255, 255, 255) // shadowColor
);
}
frame.padding = 6;
frame.setWidth(size);
frame.acceptsDrops = false;
frame.contents.acceptsDrops = false;
text.fontName = 'monospace';
text.fontStyle = 'monospace';
text.fontSize = 11;
text.setPosition(frame.topLeft().add(frame.padding));
text.enableSelecting();
text.isEditable = true;
frame.setHeight(size / 4);
frame.fixLayout = nop;
frame.edge = InputFieldMorph.prototype.edge;
frame.fontSize = InputFieldMorph.prototype.fontSize;
frame.typeInPadding = InputFieldMorph.prototype.typeInPadding;
frame.contrast = InputFieldMorph.prototype.contrast;
frame.drawNew = InputFieldMorph.prototype.drawNew;
frame.drawRectBorder = InputFieldMorph.prototype.drawRectBorder;
frame.addContents(text);
text.drawNew();
if (pic) {this.setPicture(pic); }
this.labelString = title;
this.createLabel();
if (!this.key) {
this.key = 'promptCode' + title + defaultString;
}
bdy.setColor(this.color);
bdy.add(frame);
if (instructions) {
bdy.add(remarkText(instructions));
}
bdy.fixLayout();
this.addBody(bdy);
frame.drawNew();
bdy.drawNew();
this.addButton('ok', 'OK');
this.addButton('cancel', 'Cancel');
this.fixLayout();
this.drawNew();
this.fixLayout();
this.popUp(world);
text.edit();
};
DialogBoxMorph.prototype.promptCredentials = function (
title,
purpose,
tosURL,
tosLabel,
prvURL,
prvLabel,
checkBoxLabel,
world,
pic,
msg
) {
var usr = new InputFieldMorph(),
bmn,
byr,
emlLabel,
eml = new InputFieldMorph(),
pw1 = new InputFieldMorph(),
pw2 = new InputFieldMorph(),
opw = new InputFieldMorph(),
agree = false,
chk,
dof = new AlignmentMorph('row', 4),
mCol = new AlignmentMorph('column', 2),
yCol = new AlignmentMorph('column', 2),
inp = new AlignmentMorph('column', 2),
lnk = new AlignmentMorph('row', 4),
bdy = new AlignmentMorph('column', this.padding),
years = {},
currentYear = new Date().getFullYear(),
firstYear = currentYear - 20,
myself = this;
function labelText(string) {
return new TextMorph(
string,
10,
null, // style
false, // bold
null, // italic
null, // alignment
null, // width
null, // font name
MorphicPreferences.isFlat ? null : new Point(1, 1),
new Color(255, 255, 255) // shadowColor
);
}
function linkButton(label, url) {
var btn = new PushButtonMorph(
myself,
function () {
window.open(url);
},
' ' + localize(label) + ' '
);
btn.fontSize = 10;
btn.corner = myself.buttonCorner;
btn.edge = myself.buttonEdge;
btn.outline = myself.buttonOutline;
btn.outlineColor = myself.buttonOutlineColor;
btn.outlineGradient = myself.buttonOutlineGradient;
btn.padding = myself.buttonPadding;
btn.contrast = myself.buttonContrast;
btn.drawNew();
btn.fixLayout();
return btn;
}
function age() {
var today = new Date().getFullYear() + new Date().getMonth() / 12,
year = +byr.getValue() || 0,
monthName = bmn.getValue(),
month,
birthday;
if (monthName instanceof Array) { // translatable
monthName = monthName[0];
}
if (isNaN(year)) {
year = 0;
}
month = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
].indexOf(monthName);
if (isNaN(month)) {
month = 0;
}
birthday = year + month / 12;
return today - birthday;
}
bmn = new InputFieldMorph(
null, // text
false, // numeric?
{
'January' : ['January'],
'February' : ['February'],
'March' : ['March'],
'April' : ['April'],
'May' : ['May'],
'June' : ['June'],
'July' : ['July'],
'August' : ['August'],
'September' : ['September'],
'October' : ['October'],
'November' : ['November'],
'December' : ['December']
},
true // read-only
);
for (currentYear; currentYear > firstYear; currentYear -= 1) {
years[currentYear.toString() + ' '] = currentYear;
}
years[firstYear + ' or before'] = '< ' + currentYear;
byr = new InputFieldMorph(
null, // text
false, // numeric?
years,
true // read-only
);
inp.alignment = 'left';
inp.setColor(this.color);
bdy.setColor(this.color);
mCol.alignment = 'left';
mCol.setColor(this.color);
yCol.alignment = 'left';
yCol.setColor(this.color);
usr.setWidth(200);
bmn.setWidth(100);
byr.contents().minWidth = 80;
byr.setWidth(80);
eml.setWidth(200);
pw1.setWidth(200);
pw2.setWidth(200);
opw.setWidth(200);
pw1.contents().text.toggleIsPassword();
pw2.contents().text.toggleIsPassword();
opw.contents().text.toggleIsPassword();
if (purpose === 'login') {
inp.add(labelText('User name:'));
inp.add(usr);
}
if (purpose === 'signup') {
inp.add(labelText('User name:'));
inp.add(usr);
mCol.add(labelText('Birth date:'));
mCol.add(bmn);
yCol.add(labelText('year:'));
yCol.add(byr);
dof.add(mCol);
dof.add(yCol);
inp.add(dof);
emlLabel = labelText('foo');
inp.add(emlLabel);
inp.add(eml);
}
if (purpose === 'login') {
inp.add(labelText('Password:'));
inp.add(pw1);
}
if (purpose === 'changePassword') {
inp.add(labelText('Old password:'));
inp.add(opw);
inp.add(labelText('New password:'));
inp.add(pw1);
inp.add(labelText('Repeat new password:'));
inp.add(pw2);
}
if (purpose === 'resetPassword') {
inp.add(labelText('User name:'));
inp.add(usr);
}
if (msg) {
bdy.add(labelText(msg));
}
bdy.add(inp);
if (tosURL || prvURL) {
bdy.add(lnk);
}
if (tosURL) {
lnk.add(linkButton(tosLabel, tosURL));
}
if (prvURL) {
lnk.add(linkButton(prvLabel, prvURL));
}
if (checkBoxLabel) {
chk = new ToggleMorph(
'checkbox',
this,
function () {agree = !agree; }, // action,
checkBoxLabel,
function () {return agree; } //query
);
chk.edge = this.buttonEdge / 2;
chk.outline = this.buttonOutline / 2;
chk.outlineColor = this.buttonOutlineColor;
chk.outlineGradient = this.buttonOutlineGradient;
chk.contrast = this.buttonContrast;
chk.drawNew();
chk.fixLayout();
bdy.add(chk);
}
dof.fixLayout();
mCol.fixLayout();
yCol.fixLayout();
inp.fixLayout();
lnk.fixLayout();
bdy.fixLayout();
this.labelString = title;
this.createLabel();
if (pic) {this.setPicture(pic); }
this.addBody(bdy);
usr.drawNew();
dof.drawNew();
mCol.drawNew();
bmn.drawNew();
yCol.drawNew();
byr.drawNew();
pw1.drawNew();
pw2.drawNew();
opw.drawNew();
eml.drawNew();
bdy.fixLayout();
this.addButton('ok', 'OK');
this.addButton('cancel', 'Cancel');
this.fixLayout();
this.drawNew();
this.fixLayout();
function validInputs() {
var checklist,
empty,
em = eml.getValue();
function indicate(morph, string) {
var bubble = new SpeechBubbleMorph(string);
bubble.isPointingRight = false;
bubble.drawNew();
bubble.popUp(
world,
morph.leftCenter().subtract(new Point(bubble.width() + 2, 0))
);
if (morph.edit) {
morph.edit();
}
}
if (purpose === 'login') {
checklist = [usr, pw1];
} else if (purpose === 'signup') {
checklist = [usr, bmn, byr, eml];
} else if (purpose === 'changePassword') {
checklist = [opw, pw1, pw2];
} else if (purpose === 'resetPassword') {
checklist = [usr];
}
empty = detect(
checklist,
function (inp) {
return !inp.getValue();
}
);
if (empty) {
indicate(empty, 'please fill out\nthis field');
return false;
}
if (purpose === 'signup') {
if (usr.getValue().length < 4) {
indicate(usr, 'User name must be four\ncharacters or longer');
return false;
}
if (em.indexOf(' ') > -1 || em.indexOf('@') === -1
|| em.indexOf('.') === -1) {
indicate(eml, 'please provide a valid\nemail address');
return false;
}
}
if (purpose === 'changePassword') {
if (pw1.getValue().length < 6) {
indicate(pw1, 'password must be six\ncharacters or longer');
return false;
}
if (pw1.getValue() !== pw2.getValue()) {
indicate(pw2, 'passwords do\nnot match');
return false;
}
}
if (purpose === 'signup') {
if (!agree) {
indicate(chk, 'please agree to\nthe TOS');
return false;
}
}
return true;
}
this.accept = function () {
if (validInputs()) {
DialogBoxMorph.prototype.accept.call(myself);
}
};
this.edit = function () {
if (purpose === 'changePassword') {
opw.edit();
} else { // 'signup', 'login', 'resetPassword'
usr.edit();
}
};
this.getInput = function () {
return {
username: usr.getValue(),
email: eml.getValue(),
oldpassword: opw.getValue(),
password: pw1.getValue(),
choice: agree
};
};
this.reactToChoice = function () {
if (purpose === 'signup') {
emlLabel.changed();
emlLabel.text = age() <= 13 ?
'E-mail address of parent or guardian:'
: 'E-mail address:';
emlLabel.drawNew();
emlLabel.changed();
}
};
this.reactToChoice(); // initialize e-mail label
if (!this.key) {
this.key = 'credentials' + title + purpose;
}
this.popUp(world);
};
DialogBoxMorph.prototype.accept = function () {
/*
if target is a function, use it as callback:
execute target as callback function with action as argument
in the environment as optionally specified.
Note: if action is also a function, instead of becoming
the argument itself it will be called to answer the argument.
for selections, Yes/No Choices etc:
else (if target is not a function):
if action is a function:
execute the action with target as environment (can be null)
for lambdafied (inline) actions
else if action is a String:
treat it as function property of target and execute it
for selector-like actions
*/
if (this.action) {
if (typeof this.target === 'function') {
if (typeof this.action === 'function') {
this.target.call(this.environment, this.action.call());
} else {
this.target.call(this.environment, this.action);
}
} else {
if (typeof this.action === 'function') {
this.action.call(this.target, this.getInput());
} else { // assume it's a String
this.target[this.action](this.getInput());
}
}
}
this.destroy();
};
DialogBoxMorph.prototype.withKey = function (key) {
this.key = key;
return this;
};
DialogBoxMorph.prototype.popUp = function (world) {
if (world) {
if (this.key) {
if (this.instances[world.stamp]) {
if (this.instances[world.stamp][this.key]) {
this.instances[world.stamp][this.key].destroy();
}
this.instances[world.stamp][this.key] = this;
} else {
this.instances[world.stamp] = {};
this.instances[world.stamp][this.key] = this;
}
}
world.add(this);
world.keyboardReceiver = this;
this.setCenter(world.center());
this.edit();
}
};
DialogBoxMorph.prototype.destroy = function () {
DialogBoxMorph.uber.destroy.call(this);
if (this.key) {
delete this.instances[this.key];
}
};
DialogBoxMorph.prototype.ok = function () {
this.accept();
};
DialogBoxMorph.prototype.cancel = function () {
this.destroy();
};
DialogBoxMorph.prototype.edit = function () {
this.children.forEach(function (c) {
if (c.edit) {
return c.edit();
}
});
};
DialogBoxMorph.prototype.getInput = function () {
if (this.body instanceof InputFieldMorph) {
return this.body.getValue();
}
return null;
};
DialogBoxMorph.prototype.justDropped = function (hand) {
hand.world.keyboardReceiver = this;
this.edit();
};
DialogBoxMorph.prototype.destroy = function () {
var world = this.world();
world.keyboardReceiver = null;
world.hand.destroyTemporaries();
DialogBoxMorph.uber.destroy.call(this);
};
DialogBoxMorph.prototype.normalizeSpaces = function (string) {
var ans = '', i, c, flag = false;
for (i = 0; i < string.length; i += 1) {
c = string[i];
if (c === ' ') {
if (flag) {
ans += c;
flag = false;
}
} else {
ans += c;
flag = true;
}
}
return ans.trim();
};
// DialogBoxMorph submorph construction
DialogBoxMorph.prototype.createLabel = function () {
var shading = !MorphicPreferences.isFlat || this.is3D;
if (this.label) {
this.label.destroy();
}
if (this.labelString) {
this.label = new StringMorph(
localize(this.labelString),
this.titleFontSize,
this.fontStyle,
true,
false,
false,
shading ? new Point(2, 1) : null,
this.titleBarColor.darker(this.contrast)
);
this.label.color = this.titleTextColor;
this.label.drawNew();
this.add(this.label);
}
};
DialogBoxMorph.prototype.createButtons = function () {
if (this.buttons) {
this.buttons.destroy();
}
this.buttons = new AlignmentMorph('row', this.padding);
this.add(this.buttons);
};
DialogBoxMorph.prototype.addButton = function (action, label) {
var button = new PushButtonMorph(
this,
action || 'ok',
' ' + localize((label || 'OK')) + ' '
);
button.fontSize = this.buttonFontSize;
button.corner = this.buttonCorner;
button.edge = this.buttonEdge;
button.outline = this.buttonOutline;
button.outlineColor = this.buttonOutlineColor;
button.outlineGradient = this.buttonOutlineGradient;
button.padding = this.buttonPadding;
button.contrast = this.buttonContrast;
button.drawNew();
button.fixLayout();
this.buttons.add(button);
return button;
};
DialogBoxMorph.prototype.setPicture = function (aMorphOrCanvas) {
var morph;
if (aMorphOrCanvas instanceof Morph) {
morph = aMorphOrCanvas;
} else {
morph = new Morph();
morph.image = aMorphOrCanvas;
morph.silentSetWidth(aMorphOrCanvas.width);
morph.silentSetHeight(aMorphOrCanvas.height);
}
this.addHead(morph);
};
DialogBoxMorph.prototype.addHead = function (aMorph) {
if (this.head) {
this.head.destroy();
}
this.head = aMorph;
this.add(this.head);
};
DialogBoxMorph.prototype.addBody = function (aMorph) {
if (this.body) {
this.body.destroy();
}
this.body = aMorph;
this.add(this.body);
};
// DialogBoxMorph layout
DialogBoxMorph.prototype.addShadow = function () {nop(); };
DialogBoxMorph.prototype.removeShadow = function () {nop(); };
DialogBoxMorph.prototype.fixLayout = function () {
var th = fontHeight(this.titleFontSize) + this.titlePadding * 2, w;
if (this.head) {
this.head.setPosition(this.position().add(new Point(
this.padding,
th + this.padding
)));
this.silentSetWidth(this.head.width() + this.padding * 2);
this.silentSetHeight(
this.head.height()
+ this.padding * 2
+ th
);
}
if (this.body) {
if (this.head) {
this.body.setPosition(this.head.bottomLeft().add(new Point(
0,
this.padding
)));
this.silentSetWidth(Math.max(
this.width(),
this.body.width() + this.padding * 2
));
this.silentSetHeight(
this.height()
+ this.body.height()
+ this.padding
);
w = this.width();
this.head.setLeft(
this.left()
+ Math.round((w - this.head.width()) / 2)
);
this.body.setLeft(
this.left()
+ Math.round((w - this.body.width()) / 2)
);
} else {
this.body.setPosition(this.position().add(new Point(
this.padding,
th + this.padding
)));
this.silentSetWidth(this.body.width() + this.padding * 2);
this.silentSetHeight(
this.body.height()
+ this.padding * 2
+ th
);
}
}
if (this.label) {
this.label.setCenter(this.center());
this.label.setTop(this.top() + (th - this.label.height()) / 2);
}
if (this.buttons && (this.buttons.children.length > 0)) {
this.buttons.fixLayout();
this.silentSetHeight(
this.height()
+ this.buttons.height()
+ this.padding
);
this.buttons.setCenter(this.center());
this.buttons.setBottom(this.bottom() - this.padding);
}
};
// DialogBoxMorph shadow
/*
only take the 'plain' image, so the box rounding doesn't become
conflicted by the scrolling scripts pane
*/
DialogBoxMorph.prototype.shadowImage = function (off, color) {
// fallback for Windows Chrome-Shadow bug
var fb, img, outline, sha, ctx,
offset = off || new Point(7, 7),
clr = color || new Color(0, 0, 0);
fb = this.extent();
img = this.image;
outline = newCanvas(fb);
ctx = outline.getContext('2d');
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(
img,
-offset.x,
-offset.y
);
sha = newCanvas(fb);
ctx = sha.getContext('2d');
ctx.drawImage(outline, 0, 0);
ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = clr.toString();
ctx.fillRect(0, 0, fb.x, fb.y);
return sha;
};
DialogBoxMorph.prototype.shadowImageBlurred = function (off, color) {
var fb, img, sha, ctx,
offset = off || new Point(7, 7),
blur = this.shadowBlur,
clr = color || new Color(0, 0, 0);
fb = this.extent().add(blur * 2);
img = this.image;
sha = newCanvas(fb);
ctx = sha.getContext('2d');
ctx.shadowOffsetX = offset.x;
ctx.shadowOffsetY = offset.y;
ctx.shadowBlur = blur;
ctx.shadowColor = clr.toString();
ctx.drawImage(
img,
blur - offset.x,
blur - offset.y
);
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(
img,
blur - offset.x,
blur - offset.y
);
return sha;
};
// DialogBoxMorph keyboard events
DialogBoxMorph.prototype.processKeyPress = function () {nop(); };
DialogBoxMorph.prototype.processKeyDown = function (event) {
// this.inspectKeyEvent(event);
switch (event.keyCode) {
case 13:
this.ok();
break;
case 27:
this.cancel();
break;
default:
// this.inspectKeyEvent(event);
}
};
// DialogBoxMorph drawing
DialogBoxMorph.prototype.drawNew = function () {
this.fullChanged();
Morph.prototype.trackChanges = false;
DialogBoxMorph.uber.removeShadow.call(this);
this.fixLayout();
var context,
gradient,
w = this.width(),
h = this.height(),
th = Math.floor(
fontHeight(this.titleFontSize) + this.titlePadding * 2
),
shift = this.corner / 2,
x,
y,
isFlat = MorphicPreferences.isFlat && !this.is3D;
// this.alpha = isFlat ? 0.9 : 1;
this.image = newCanvas(this.extent());
context = this.image.getContext('2d');
// title bar
if (isFlat) {
context.fillStyle = this.titleBarColor.toString();
} else {
gradient = context.createLinearGradient(0, 0, 0, th);
gradient.addColorStop(
0,
this.titleBarColor.lighter(this.contrast / 2).toString()
);
gradient.addColorStop(
1,
this.titleBarColor.darker(this.contrast).toString()
);
context.fillStyle = gradient;
}
context.beginPath();
this.outlinePathTitle(
context,
isFlat ? 0 : this.corner
);
context.closePath();
context.fill();
// flat shape
// body
context.fillStyle = this.color.toString();
context.beginPath();
this.outlinePathBody(
context,
isFlat ? 0 : this.corner
);
context.closePath();
context.fill();
if (isFlat) {
DialogBoxMorph.uber.addShadow.call(this);
Morph.prototype.trackChanges = true;
this.fullChanged();
return;
}
// 3D-effect
// bottom left corner
gradient = context.createLinearGradient(
0,
h - this.corner,
0,
h
);
gradient.addColorStop(0, this.color.toString());
gradient.addColorStop(1, this.color.darker(this.contrast.toString()));
context.lineWidth = this.corner;
context.lineCap = 'round';
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(this.corner, h - shift);
context.lineTo(this.corner + 1, h - shift);
context.stroke();
// bottom edge
gradient = context.createLinearGradient(
0,
h - this.corner,
0,
h
);
gradient.addColorStop(0, this.color.toString());
gradient.addColorStop(1, this.color.darker(this.contrast.toString()));
context.lineWidth = this.corner;
context.lineCap = 'butt';
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(this.corner, h - shift);
context.lineTo(w - this.corner, h - shift);
context.stroke();
// right body edge
gradient = context.createLinearGradient(
w - this.corner,
0,
w,
0
);
gradient.addColorStop(0, this.color.toString());
gradient.addColorStop(1, this.color.darker(this.contrast).toString());
context.lineWidth = this.corner;
context.lineCap = 'butt';
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(w - shift, th);
context.lineTo(w - shift, h - this.corner);
context.stroke();
// bottom right corner
x = w - this.corner;
y = h - this.corner;
gradient = context.createRadialGradient(
x,
y,
0,
x,
y,
this.corner
);
gradient.addColorStop(0, this.color.toString());
gradient.addColorStop(1, this.color.darker(this.contrast.toString()));
context.lineCap = 'butt';
context.strokeStyle = gradient;
context.beginPath();
context.arc(
x,
y,
shift,
radians(90),
radians(0),
true
);
context.stroke();
// left body edge
gradient = context.createLinearGradient(
0,
0,
this.corner,
0
);
gradient.addColorStop(1, this.color.toString());
gradient.addColorStop(
0,
this.color.lighter(this.contrast).toString()
);
context.lineCap = 'butt';
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(shift, th);
context.lineTo(shift, h - this.corner * 2);
context.stroke();
// left vertical bottom corner
gradient = context.createLinearGradient(
0,
0,
this.corner,
0
);
gradient.addColorStop(1, this.color.toString());
gradient.addColorStop(
0,
this.color.lighter(this.contrast).toString()
);
context.lineCap = 'round';
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(shift, h - this.corner * 2);
context.lineTo(shift, h - this.corner - shift);
context.stroke();
DialogBoxMorph.uber.addShadow.call(this);
Morph.prototype.trackChanges = true;
this.fullChanged();
};
DialogBoxMorph.prototype.outlinePathTitle = function (context, radius) {
var w = this.width(),
h = Math.ceil(fontHeight(this.titleFontSize)) + this.titlePadding * 2;
// top left:
context.arc(
radius,
radius,
radius,
radians(-180),
radians(-90),
false
);
// top right:
context.arc(
w - radius,
radius,
radius,
radians(-90),
radians(-0),
false
);
// bottom right:
context.lineTo(w, h);
// bottom left:
context.lineTo(0, h);
};
DialogBoxMorph.prototype.outlinePathBody = function (context, radius) {
var w = this.width(),
h = this.height(),
th = Math.floor(fontHeight(this.titleFontSize)) +
this.titlePadding * 2;
// top left:
context.moveTo(0, th);
// top right:
context.lineTo(w, th);
// bottom right:
context.arc(
w - radius,
h - radius,
radius,
radians(0),
radians(90),
false
);
// bottom left:
context.arc(
radius,
h - radius,
radius,
radians(90),
radians(180),
false
);
};
// AlignmentMorph /////////////////////////////////////////////////////
// I am a reified layout, either a row or a column of submorphs
// AlignmentMorph inherits from Morph:
AlignmentMorph.prototype = new Morph();
AlignmentMorph.prototype.constructor = AlignmentMorph;
AlignmentMorph.uber = Morph.prototype;
// AlignmentMorph instance creation:
function AlignmentMorph(orientation, padding) {
this.init(orientation, padding);
}
AlignmentMorph.prototype.init = function (orientation, padding) {
// additional properties:
this.orientation = orientation || 'row'; // or 'column'
this.alignment = 'center'; // or 'left' in a column
this.padding = padding || 0;
this.respectHiddens = false;
// initialize inherited properties:
AlignmentMorph.uber.init.call(this);
// override inherited properites:
};
// AlignmentMorph displaying and layout
AlignmentMorph.prototype.drawNew = function () {
this.image = newCanvas(new Point(1, 1));
this.fixLayout();
};
AlignmentMorph.prototype.fixLayout = function () {
var myself = this,
last = null,
newBounds;
if (this.children.length === 0) {
return null;
}
this.children.forEach(function (c) {
var cfb = c.fullBounds(),
lfb;
if (c.isVisible || myself.respectHiddens) {
if (last) {
lfb = last.fullBounds();
if (myself.orientation === 'row') {
c.setPosition(
lfb.topRight().add(new Point(
myself.padding,
(lfb.height() - cfb.height()) / 2
))
);
} else { // orientation === 'column'
c.setPosition(
lfb.bottomLeft().add(new Point(
myself.alignment === 'center' ?
(lfb.width() - cfb.width()) / 2
: 0,
myself.padding
))
);
}
newBounds = newBounds.merge(cfb);
} else {
newBounds = cfb;
}
last = c;
}
});
this.bounds = newBounds;
};
// InputFieldMorph //////////////////////////////////////////////////////
// InputFieldMorph inherits from Morph:
InputFieldMorph.prototype = new Morph();
InputFieldMorph.prototype.constructor = InputFieldMorph;
InputFieldMorph.uber = Morph.prototype;
// InputFieldMorph settings
InputFieldMorph.prototype.edge = 2;
InputFieldMorph.prototype.fontSize = 12;
InputFieldMorph.prototype.typeInPadding = 2;
InputFieldMorph.prototype.contrast = 65;
// InputFieldMorph instance creation:
function InputFieldMorph(text, isNumeric, choiceDict, isReadOnly) {
this.init(text, isNumeric, choiceDict, isReadOnly);
}
InputFieldMorph.prototype.init = function (
text,
isNumeric,
choiceDict,
isReadOnly
) {
var contents = new StringFieldMorph(text || ''),
arrow = new ArrowMorph(
'down',
0,
Math.max(Math.floor(this.fontSize / 6), 1)
);
this.choices = choiceDict || null; // object, function or selector
this.isReadOnly = isReadOnly || false;
this.isNumeric = isNumeric || false;
contents.alpha = 0;
contents.fontSize = this.fontSize;
contents.drawNew();
this.oldContentsExtent = contents.extent();
this.isNumeric = isNumeric || false;
InputFieldMorph.uber.init.call(this);
this.color = new Color(255, 255, 255);
this.add(contents);
this.add(arrow);
contents.isDraggable = false;
this.drawNew();
};
// InputFieldMorph accessing:
InputFieldMorph.prototype.contents = function () {
return detect(
this.children,
function (child) {
return (child instanceof StringFieldMorph);
}
);
};
InputFieldMorph.prototype.arrow = function () {
return detect(
this.children,
function (child) {
return (child instanceof ArrowMorph);
}
);
};
InputFieldMorph.prototype.setChoice = function (aStringOrFloat) {
this.setContents(aStringOrFloat);
this.escalateEvent('reactToChoice', aStringOrFloat);
};
InputFieldMorph.prototype.setContents = function (aStringOrFloat) {
var cnts = this.contents();
cnts.text.text = aStringOrFloat;
if (aStringOrFloat === undefined) {
return null;
}
if (aStringOrFloat === null) {
cnts.text.text = '';
} else if (aStringOrFloat.toString) {
cnts.text.text = aStringOrFloat.toString();
}
cnts.drawNew();
cnts.changed();
};
InputFieldMorph.prototype.edit = function () {
var c = this.contents();
c.text.edit();
c.text.selectAll();
};
InputFieldMorph.prototype.setIsNumeric = function (bool) {
var value;
this.isNumeric = bool;
this.contents().isNumeric = bool;
this.contents().text.isNumeric = bool;
// adjust my shown value to conform with the numeric flag
value = this.getValue();
if (this.isNumeric) {
value = parseFloat(value);
if (isNaN(value)) {
value = null;
}
}
this.setContents(value);
};
// InputFieldMorph drop-down menu:
InputFieldMorph.prototype.dropDownMenu = function () {
var choices = this.choices,
key,
menu = new MenuMorph(
this.setChoice,
null,
this,
this.fontSize
);
if (choices instanceof Function) {
choices = choices.call(this);
} else if (isString(choices)) {
choices = this[choices]();
}
if (!choices) {
return null;
}
menu.addItem(' ', null);
if (choices instanceof Array) {
choices.forEach(function (choice) {
menu.addItem(choice[0], choice[1]);
});
} else { // assuming a dictionary
for (key in choices) {
if (Object.prototype.hasOwnProperty.call(choices, key)) {
if (key[0] === '~') {
menu.addLine();
} else {
menu.addItem(key, choices[key]);
}
}
}
}
if (menu.items.length > 0) {
menu.popUpAtHand(this.world());
} else {
return null;
}
};
// InputFieldMorph layout:
InputFieldMorph.prototype.fixLayout = function () {
var contents = this.contents(),
arrow = this.arrow();
if (!contents) {return null; }
contents.isNumeric = this.isNumeric;
contents.isEditable = (!this.isReadOnly);
if (this.choices) {
arrow.setSize(this.fontSize);
arrow.show();
} else {
arrow.setSize(0);
arrow.hide();
}
this.silentSetHeight(
contents.height()
+ this.edge * 2
+ this.typeInPadding * 2
);
this.silentSetWidth(Math.max(
contents.minWidth
+ this.edge * 2
+ this.typeInPadding * 2,
this.width()
));
contents.setWidth(
this.width() - this.edge - this.typeInPadding -
(this.choices ? arrow.width() + this.typeInPadding : 0)
);
contents.silentSetPosition(new Point(
this.edge,
this.edge
).add(this.typeInPadding).add(this.position()));
arrow.silentSetPosition(new Point(
this.right() - arrow.width() - this.edge,
contents.top()
));
};
// InputFieldMorph events:
InputFieldMorph.prototype.mouseClickLeft = function (pos) {
if (this.arrow().bounds.containsPoint(pos)) {
this.dropDownMenu();
} else if (this.isReadOnly) {
this.dropDownMenu();
} else {
this.escalateEvent('mouseClickLeft', pos);
}
};
// InputFieldMorph retrieving:
InputFieldMorph.prototype.getValue = function () {
/*
answer my content's text string. If I am numerical convert that
string to a number. If the conversion fails answer the string
otherwise the numerical value.
*/
var num,
contents = this.contents();
if (this.isNumeric) {
num = parseFloat(contents.text);
if (!isNaN(num)) {
return num;
}
}
return this.normalizeSpaces(contents.string());
};
InputFieldMorph.prototype.normalizeSpaces
= DialogBoxMorph.prototype.normalizeSpaces;
// InputFieldMorph drawing:
InputFieldMorph.prototype.drawNew = function () {
var context, borderColor;
this.fixLayout();
// initialize my surface property
this.image = newCanvas(this.extent());
context = this.image.getContext('2d');
if (this.parent) {
if (this.parent.color.eq(new Color(255, 255, 255))) {
this.color = this.parent.color.darker(this.contrast * 0.1);
} else {
this.color = this.parent.color.lighter(this.contrast * 0.75);
}
borderColor = this.parent.color;
} else {
borderColor = new Color(120, 120, 120);
}
context.fillStyle = this.color.toString();
// cache my border colors
this.cachedClr = borderColor.toString();
this.cachedClrBright = borderColor.lighter(this.contrast)
.toString();
this.cachedClrDark = borderColor.darker(this.contrast).toString();
context.fillRect(
this.edge,
this.edge,
this.width() - this.edge * 2,
this.height() - this.edge * 2
);
this.drawRectBorder(context);
};
InputFieldMorph.prototype.drawRectBorder = function (context) {
var shift = this.edge * 0.5,
gradient;
if (MorphicPreferences.isFlat && !this.is3D) {return; }
context.lineWidth = this.edge;
context.lineJoin = 'round';
context.lineCap = 'round';
context.shadowOffsetY = shift;
context.shadowBlur = this.edge * 4;
context.shadowColor = this.cachedClrDark;
gradient = context.createLinearGradient(
0,
0,
0,
this.edge
);
gradient.addColorStop(0, this.cachedClr);
gradient.addColorStop(1, this.cachedClrDark);
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(this.edge, shift);
context.lineTo(this.width() - this.edge - shift, shift);
context.stroke();
context.shadowOffsetY = 0;
gradient = context.createLinearGradient(
0,
0,
this.edge,
0
);
gradient.addColorStop(0, this.cachedClr);
gradient.addColorStop(1, this.cachedClrDark);
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(shift, this.edge);
context.lineTo(shift, this.height() - this.edge - shift);
context.stroke();
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.shadowBlur = 0;
gradient = context.createLinearGradient(
0,
this.height() - this.edge,
0,
this.height()
);
gradient.addColorStop(0, this.cachedClrBright);
gradient.addColorStop(1, this.cachedClr);
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(this.edge, this.height() - shift);
context.lineTo(this.width() - this.edge, this.height() - shift);
context.stroke();
gradient = context.createLinearGradient(
this.width() - this.edge,
0,
this.width(),
0
);
gradient.addColorStop(0, this.cachedClrBright);
gradient.addColorStop(1, this.cachedClr);
context.strokeStyle = gradient;
context.beginPath();
context.moveTo(this.width() - shift, this.edge);
context.lineTo(this.width() - shift, this.height() - this.edge);
context.stroke();
};