UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

1,014 lines (932 loc) 39 kB
/* Copyright The Infusion copyright holders See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-project/infusion/raw/master/AUTHORS.md. Licensed under the Educational Community License (ECL), Version 2.0 or the New BSD license. You may not use this file except in compliance with one these Licenses. You may obtain a copy of the ECL 2.0 License and BSD License at https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt */ var fluid_3_0_0 = fluid_3_0_0 || {}; (function ($, fluid) { "use strict"; /********************************************** * fluid.orator * * A component for self voicing a web page **********************************************/ fluid.defaults("fluid.orator", { gradeNames: ["fluid.viewComponent"], selectors: { controller: ".flc-orator-controller", content: ".flc-orator-content" }, model: { enabled: true, play: false }, components: { tts: { type: "fluid.textToSpeech" }, controller: { type: "fluid.orator.controller", options: { parentContainer: "{orator}.container", model: { playing: "{orator}.model.play", enabled: "{orator}.model.enabled" } } }, selectionReader: { type: "fluid.orator.selectionReader", container: "{that}.container", options: { model: { enabled: "{orator}.model.enabled" } } }, domReader: { type: "fluid.orator.domReader", container: "{that}.dom.content", options: { model: { tts: { enabled: "{orator}.model.enabled" } }, listeners: { "utteranceOnEnd.domReaderStop": { changePath: "{orator}.model.play", value: false, source: "domReader.utteranceOnEnd", priority: "after:removeHighlight" } }, modelListeners: { "{orator}.model.play": { funcName: "fluid.orator.handlePlayToggle", args: ["{that}", "{change}.value"], namespace: "domReader.handlePlayToggle" } } } } }, modelListeners: { "enabled": { listener: "fluid.orator.cancelWhenDisabled", args: ["{tts}.cancel", "{change}.value"], namespace: "orator.clearSpeech" } }, distributeOptions: [{ source: "{that}.options.tts", target: "{that tts}.options", removeSource: true, namespace: "ttsOpts" }, { source: "{that}.options.controller", target: "{that controller}.options", removeSource: true, namespace: "controllerOpts" }, { source: "{that}.options.domReader", target: "{that domReader}.options", removeSource: true, namespace: "domReaderOpts" }, { source: "{that}.options.selectionReader", target: "{that selectionReader}.options", removeSource: true, namespace: "selectionReaderOpts" }] }); // TODO: When https://issues.fluidproject.org/browse/FLUID-6393 has been addressed, it will be possible to remove // this function and directly configure the modelListener to only trigger when a false value is passed. fluid.orator.cancelWhenDisabled = function (cancelFn, state) { if (!state) { cancelFn(); } }; fluid.orator.handlePlayToggle = function (that, state) { if (state) { that.play(); } else { that.pause(); } }; /********************************************** * fluid.orator.controller * * Provides a UI Widget to control the Orator **********************************************/ fluid.defaults("fluid.orator.controller", { gradeNames: ["fluid.containerRenderingView"], selectors: { playToggle: ".flc-orator-controller-playToggle" }, styles: { play: "fl-orator-controller-play" }, strings: { play: "play", pause: "pause" }, model: { playing: false, enabled: true }, injectionType: "prepend", markup: { container: "<div class=\"flc-orator-controller fl-orator-controller\">" + "<div class=\"fl-icon-orator\" aria-hidden=\"true\"></div>" + "<button class=\"flc-orator-controller-playToggle\">" + "<span class=\"fl-orator-controller-playToggle fl-icon-orator-playToggle\" aria-hidden=\"true\"></span>" + "</button></div>" }, invokers: { play: { changePath: "playing", value: true, source: "play" }, pause: { changePath: "playing", value: false, source: "pause" }, toggle: { funcName: "fluid.orator.controller.toggleState", args: ["{that}", "{arguments}.0", "{arguments}.1"] } }, listeners: { "onCreate.bindClick": { listener: "fluid.orator.controller.bindClick", args: ["{that}"] } }, modelListeners: { "playing": { listener: "fluid.orator.controller.setToggleView", args: ["{that}", "{change}.value"] }, "enabled": { "this": "{that}.container", method: "toggle", args: ["{change}.value"], namespace: "toggleView" } } }); /** * Binds the click event for the "playToggle" element to trigger the `that.toggle` method. * This is not bound declaratively to ensure that the correct arguments are passed along to the `that.toggle` * method. * * @param {Component} that - an instance of `fluid.orator.controller` */ fluid.orator.controller.bindClick = function (that) { that.locate("playToggle").click(function () { that.toggle("playing"); }); }; /** * Used to toggle the state of a model value at a specified path. The new state will be the inverse of the current * boolean value at the specified model path, or can be set explicitly by passing in a 'state' value. It's likely * that this method will be used in conjunction with a click handler. In that case it's most likely that the state * will be toggling the existing model value. * * @param {Component} that - an instance of `fluid.orator.controller` * @param {String|Array} path - the path, into the model, for the value to toggle * @param {Boolean} state - (optional) explicit state to set the model value to */ fluid.orator.controller.toggleState = function (that, path, state) { var newState = fluid.isValue(state) ? state : !fluid.get(that.model, path); // the !! ensures that the newState is a boolean value. that.applier.change(path, !!newState, "ADD", "toggleState"); }; /** * Sets the view state of the toggle controller. * True - play style added * - aria-label set to the `pause` string * False - play style removed * - aria-label set to the `play` string * * @param {Component} that - an instance of `fluid.orator.controller` * @param {Boolean} state - the state to set the controller to */ fluid.orator.controller.setToggleView = function (that, state) { var playToggle = that.locate("playToggle"); playToggle.toggleClass(that.options.styles.play, state); playToggle.attr({ "aria-label": that.options.strings[state ? "pause" : "play"] }); }; /******************************************************************************* * fluid.orator.domReader * * Reads in text from a DOM element and voices it *******************************************************************************/ fluid.defaults("fluid.orator.domReader", { gradeNames: ["fluid.viewComponent"], selectors: { highlight: ".flc-orator-highlight" }, markup: { highlight: "<mark class=\"flc-orator-highlight fl-orator-highlight\"></mark>" }, events: { onQueueSpeech: null, onReadFromDOM: null, utteranceOnEnd: null, utteranceOnBoundary: null, utteranceOnError: null, utteranceOnMark: null, utteranceOnPause: null, utteranceOnResume: null, utteranceOnStart: null }, utteranceEventMap: { onboundary: "utteranceOnBoundary", onend: "utteranceOnEnd", onerror: "utteranceOnError", onmark:"utteranceOnMark", onpause: "utteranceOnPause", onresume: "utteranceOnResume", onstart: "utteranceOnStart" }, model: { tts: { paused: false, speaking: false, enabled: true }, parseQueueLength: 0, parseIndex: null, ttsBoundary: null }, modelRelay: [{ target: "parseIndex", backward: "never", namespace: "getClosestIndex", singleTransform: { type: "fluid.transforms.free", func: "fluid.orator.domReader.getClosestIndex", args: ["{that}", "{that}.model.ttsBoundary"] } }], members: { parseQueue: [], range: { expander: { this: "document", method: "createRange" } } }, components: { parser: { type: "fluid.textNodeParser", options: { invokers: { hasTextToRead: "fluid.textNodeParser.hasVisibleText" }, listeners: { "onParsedTextNode.addToParseQueue": "{domReader}.addToParseQueue" } } } }, invokers: { parsedToString: "fluid.orator.domReader.parsedToString", readFromDOM: { funcName: "fluid.orator.domReader.readFromDOM", args: ["{that}", "{that}.container"] }, removeHighlight: { funcName: "fluid.orator.domReader.unWrap", args: ["{that}.dom.highlight"] }, addToParseQueue: { funcName: "fluid.orator.domReader.addToParseQueue", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, resetParseQueue: { funcName: "fluid.orator.domReader.resetParseQueue", args: ["{that}"] }, highlight: { funcName: "fluid.orator.domReader.highlight", args: ["{that}", "{arguments}.0"] }, play: { funcName: "fluid.orator.domReader.play", args: ["{that}", "{fluid.textToSpeech}.resume"] }, pause: { funcName: "fluid.orator.domReader.pause", args: ["{that}", "{fluid.textToSpeech}.pause"] }, queueSpeech: { funcName: "fluid.orator.domReader.queueSpeech", args: ["{that}", "{arguments}.0", true, "{arguments}.1"] }, isWord: "fluid.textNodeParser.isWord" }, modelListeners: { "highlight": { listener: "{that}.highlight", path: ["parseIndex", "parseQueueLength"] } }, listeners: { "onQueueSpeech.removeExtraWhiteSpace": "fluid.orator.domReader.removeExtraWhiteSpace", "onQueueSpeech.queueSpeech": { func: "{fluid.textToSpeech}.queueSpeech", args: ["{arguments}.0", "{arguments}.1.interrupt", "{arguments}.1"], priority: "after:removeExtraWhiteSpace" }, "utteranceOnEnd.resetParseQueue": { listener: "{that}.resetParseQueue" }, "utteranceOnEnd.removeHighlight": { listener: "{that}.removeHighlight", priority: "after:resetParseQueue" }, "utteranceOnEnd.updateTTSModel": { changePath: "tts", value: { speaking: false, paused: false } }, "utteranceOnStart.updateTTSModel": { changePath: "tts", value: { speaking: true, paused: false } }, "utteranceOnPause.updateTTSModel": { changePath: "tts", value: { speaking: true, paused: true } }, "utteranceOnResume.updateTTSModel": { changePath: "tts", value: { speaking: true, paused: false } }, "utteranceOnBoundary.setCurrentBoundary": { changePath: "ttsBoundary", value: "{arguments}.0.charIndex", source: "utteranceOnBoundary" }, "onDestroy.detachRange": { "this": "{that}.range", method: "detach" } } }); fluid.orator.domReader.play = function (that, resumeFn) { if (that.model.tts.enabled) { if (that.model.tts.paused) { resumeFn(); } else if (!that.model.tts.speaking) { that.readFromDOM(); } } }; fluid.orator.domReader.pause = function (that, pauseFn) { if (that.model.tts.speaking && !that.model.tts.paused) { pauseFn(); } }; fluid.orator.domReader.mapUtteranceEvents = function (that, utterance, utteranceEventMap) { fluid.each(utteranceEventMap, function (compEventName, utteranceEvent) { var compEvent = that.events[compEventName]; utterance[utteranceEvent] = compEvent.fire; }); }; fluid.orator.domReader.removeExtraWhiteSpace = function (text) { var promise = fluid.promise(); // force a string value var str = text.toString(); // trim whitespace str = str.trim(); if (str) { promise.resolve(str); } else { promise.reject("The text is empty"); } return promise; }; /** * Operates the core "transforming promise workflow" for queuing an utterance. The initial listener is provided the * initial text; which then proceeds through the transform chain to arrive at the final text. * To change the speech function (e.g for testing) the onQueueSpeech.queueSpeech listener can be overridden. * * @param {Component} that - an instance of `fluid.orator.domReader` * @param {String} text - The text to be synthesized * @param {Boolean} interrupt - Used to indicate if this text should be queued or replace existing utterances. * This will be passed along to the listeners in the options; `options.interrupt`. * @param {Object} options - (optional) options to configure the utterance with. This will also be interpolated with * the interrupt parameter and event mappings. See: fluid.textToSpeech.queueSpeech in * TextToSpeech.js for an example of utterance options for that speech function. * * @return {Promise} - A promise for the final resolved text */ fluid.orator.domReader.queueSpeech = function (that, text, interrupt, options) { options = options || {}; options.interrupt = interrupt || options.interrupt; // map events fluid.orator.domReader.mapUtteranceEvents(that, options, that.options.utteranceEventMap); return fluid.promise.fireTransformEvent(that.events.onQueueSpeech, text, options); }; /** * Unwraps the contents of the element by removing the tag surrounding the content and placing the content * as a node within the element's parent. The parent is also normalized to combine any adjacent textnodes. * * @param {String|jQuery|DomElement} elm - element to unwrap */ fluid.orator.domReader.unWrap = function (elm) { elm = $(elm); if (elm.length) { var parent = elm.parent(); // Remove the element, but place its contents within the parent. elm.contents().unwrap(); // Normalize the parent to cleanup textnodes parent[0].normalize(); } }; /** * Positional information about a word parsed from the text in a {DomElement}. This can be used for mappings between * a synthesizer's speech boundary and the word's location within the DOM. * * @typedef {Object} DomWordMap * @property {Integer} blockIndex - The index into the entire block of text being parsed from the DOM * @property {Integer} startOffset - The start offset of the current `word` relative to the closest * enclosing DOM element * @property {Integer} endOffset - The end offset of the current `word` relative to the closest * enclosing DOM element * @property {DomNode} node - The current child node being parsed * @property {Integer} childIndex - The index of the child node being parsed relative to its parent * @property {DomElement} parentNode - The parent DOM node * @property {String} word - The text, `word`, parsed from the node. It may contain only whitespace. */ /** * Takes in a textnode and separates the contained words into DomWordMaps that are added to the parseQueue. * Typically this handles parsed data passed along by a Parser's (`fluid.textNodeParser`) `onParsedTextNode` event. * Empty nodes are skipped and the subsequent text is analyzed to determine if it should be appended to the * previous DomWordMap in the parseQueue. For example: when the syllabification separator is tag is inserted * between words. * * @param {Component} that - an instance of `fluid.orator.domReader` * @param {DomNode} textNode - the text node being parsed * @param {String} lang - a valid BCP 47 language code. * @param {Integer} childIndex - the index of the text node within its parent's set of child nodes */ fluid.orator.domReader.addToParseQueue = function (that, textNode, lang, childIndex) { var lastParsed = that.parseQueue[that.parseQueue.length - 1] || {}; var words = textNode.textContent.split(/(\s+)/); // split on whitespace, and capture whitespace var parsed = { blockIndex: (lastParsed.blockIndex || 0) + (fluid.get(lastParsed, ["word", "length"]) || 0), startOffset: 0, node: textNode, childIndex: childIndex, parentNode: textNode.parentNode, lang: lang }; fluid.each(words, function (word) { var lastIsWord = that.isWord(lastParsed.word); var currentIsWord = that.isWord(word); // If the last parsed item is a word and the current item is a word, combine into the the last parsed block. // Otherwise, if the new item is a word or non-empty string create a new parsed block. if (lastIsWord && currentIsWord) { lastParsed.word += word; lastParsed.endOffset += word.length; parsed.blockIndex += word.length; parsed.startOffset += word.length; } else { parsed.word = word; parsed.endOffset = parsed.startOffset + word.length; if (currentIsWord || word && lastIsWord) { lastParsed = fluid.copy(parsed); that.parseQueue.push(lastParsed); that.applier.change("parseQueueLength", that.parseQueue.length, "ADD", "addToParseQueue"); parsed.blockIndex += word.length; } parsed.startOffset = parsed.endOffset; } }); }; /** * Empty the parseQueue and related model values * @param {Component} that - an instance of `fluid.orator.domReader` */ fluid.orator.domReader.resetParseQueue = function (that) { that.parseQueue = []; that.applier.change("", { parseQueueLength: 0, parseIndex: null, ttsBoundary: null }, "ADD", "resetParseQueue"); }; /** * Combines the parsed text into a String. * * @param {DomWordMap[]} parsed - An array of {DomWordMap} objects containing the position mappings from a parsed * {DomElement}. * * @return {String} - The parsed text combined into a String. */ fluid.orator.domReader.parsedToString = function (parsed) { var words = fluid.transform(parsed, function (block) { return block.word; }); return words.join(""); }; /** * Parses the DOM element into data points to use for highlighting the text, and queues the text into the self * voicing engine. The parsed data points are added to the component's `parseQueue` * * @param {Component} that - an instance of `fluid.orator.domReader` * @param {String|jQuery|DomElement} elm - The DOM node to read */ fluid.orator.domReader.readFromDOM = function (that, elm) { elm = $(elm); // only execute if there are nodes to read from if (elm.length) { that.resetParseQueue(); that.parser.parse(elm[0]); that.queueSpeech(that.parsedToString(that.parseQueue)); } }; /** * Returns the index of the closest data point from the parseQueue based on the boundary provided. * * @param {Component} that - an instance of `fluid.orator.domReader` * @param {Integer} boundary - The boundary value used to compare against the blockIndex of the parsed data points. * If the boundary is undefined or out of bounds, `undefined` will be returned. * * @return {Integer|undefined} - Will return the index of the closest data point in the parseQueue. If the boundary * cannot be located within the parseQueue, `undefined` is returned. */ fluid.orator.domReader.getClosestIndex = function (that, boundary) { var parseQueue = that.parseQueue; if (!parseQueue.length || !fluid.isValue(boundary)) { return undefined; }; var maxIndex = Math.max(parseQueue.length - 1, 0); var index = Math.max(Math.min(that.model.parseIndex || 0, maxIndex), 0); var maxBoundary = parseQueue[maxIndex].blockIndex + parseQueue[maxIndex].word.length; if (boundary > maxBoundary || boundary < 0) { return undefined; } while (index >= 0) { var nextIndex = index + 1; var prevIndex = index - 1; var currentBlockIndex = parseQueue[index].blockIndex; var nextBlockIndex = index < maxIndex ? parseQueue[nextIndex].blockIndex : (maxBoundary + 1); // Break if the boundary lies within the current block if (boundary >= currentBlockIndex && boundary < nextBlockIndex) { break; } if (currentBlockIndex > boundary) { index = prevIndex; } else { index = nextIndex; } } return index; }; /** * Searches down, starting from the provided node, returning the first text node found. * * @param {DomNode} node - the DOM Node to start searching from. * @return {DomNode|Undefined} - Returns the first text node found, or `undefined` if none located. */ fluid.orator.domReader.findTextNode = function (node) { if (!node) { return; } if (node.nodeType === Node.TEXT_NODE) { return node; } var children = node.childNodes; for (var i = 0; i < children.length; i++) { var textNode = fluid.orator.domReader.findTextNode(children[i]); if (textNode !== undefined) { return textNode; } } }; fluid.orator.domReader.getTextNodeFromSibling = function (node) { while (node.nextSibling) { node = node.nextSibling; var textNode = fluid.orator.domReader.findTextNode(node); if (textNode) { return textNode; } } }; fluid.orator.domReader.getNextTextNode = function (node) { var nextTextNode = fluid.orator.domReader.getTextNodeFromSibling(node); if (nextTextNode) { return nextTextNode; } var parent = node.parentNode; if (parent) { return fluid.orator.domReader.getNextTextNode(parent); } }; fluid.orator.domReader.setRangeEnd = function (range, node, end) { if (end <= node.length) { range.setEnd(node, end); } else { var nextTextNode = fluid.orator.domReader.getNextTextNode(node); fluid.orator.domReader.setRangeEnd(range, nextTextNode, end - node.length); } }; /** * Highlights text from the parseQueue. Highlights are performed by wrapping the appropriate text in the markup * specified by `that.options.markup.highlight`. * * @param {Component} that - an instance of `fluid.orator.domReader` */ fluid.orator.domReader.highlight = function (that) { that.removeHighlight(); if (that.model.parseQueueLength && fluid.isValue(that.model.parseIndex)) { var data = that.parseQueue[that.model.parseIndex]; var rangeNode = data.parentNode.childNodes[data.childIndex]; that.range.selectNode(rangeNode); that.range.setStart(rangeNode, data.startOffset); fluid.orator.domReader.setRangeEnd (that.range, rangeNode, data.endOffset); that.range.surroundContents($(that.options.markup.highlight)[0]); } }; /******************************************************************************* * fluid.orator.selectionReader * * Reads in text from a selection and voices it *******************************************************************************/ fluid.defaults("fluid.orator.selectionReader", { gradeNames: ["fluid.viewComponent"], selectors: { control: ".flc-orator-selectionReader-control", controlLabel: ".flc-orator-selectionReader-controlLabel" }, strings: { play: "play", stop: "stop" }, styles: { above: "fl-orator-selectionReader-above", below: "fl-orator-selectionReader-below", control: "fl-orator-selectionReader-control" }, markup: { control: "<button class=\"flc-orator-selectionReader-control\"><span class=\"fl-icon-orator\"></span><span class=\"flc-orator-selectionReader-controlLabel\"></span></button>" }, model: { enabled: true, showUI: false, play: false, text: "" }, // similar to em values as it will be multiplied by the container's font-size offsetScale: { edge: 3, pointer: 3 }, events: { onSelectionChanged: null, utteranceOnEnd: null, onToggleControl: null }, listeners: { "onCreate.bindEvents": { funcName: "fluid.orator.selectionReader.bindSelectionEvents", args: ["{that}"] }, "onSelectionChanged.updateText": "{that}.getSelectedText", "utteranceOnEnd.stop": { changePath: "play", value: false, source: "stopMethod" }, "onToggleControl.togglePlay": "{that}.toggle" }, modelListeners: { "showUI": { funcName: "fluid.orator.selectionReader.renderControl", args: ["{that}", "{change}.value"], namespace: "render" }, "text": { func: "{that}.stop", namespace: "stopPlayingWhenTextChanges" }, "play": [{ func: "fluid.orator.selectionReader.queueSpeech", args: ["{that}", "{change}.value", "{fluid.textToSpeech}.queueSpeech"], namespace: "queueSpeech" }, { func: "fluid.orator.selectionReader.renderControlState", args: ["{that}", "{that}.dom.control", "{arguments}.0"], namespace: "renderControlState" }], "enabled": { funcName: "fluid.orator.selectionReader.updateText", args: ["{that}", "{change}.value"], namespace: "updateText" } }, modelRelay: [{ source: "text", target: "showUI", backward: "never", namespace: "showUIControl", singleTransform: { type: "fluid.transforms.stringToBoolean" } }], invokers: { getSelectedText: { changePath: "text", value: { expander: { funcName: "fluid.orator.selectionReader.getSelectedText" } }, source: "getSelectedText" }, play: { changePath: "play", value: true, source: "playMethod" }, stop: { funcName: "fluid.orator.selectionReader.stopSpeech", args: ["{that}.model.play", "{fluid.textToSpeech}.cancel"] }, toggle: { funcName: "fluid.orator.selectionReader.togglePlay", args: ["{that}", "{arguments}.0"] } } }); fluid.orator.selectionReader.stopSpeech = function (state, cancelFn) { if (state) { cancelFn(); } }; fluid.orator.selectionReader.queueSpeech = function (that, state, speechFn) { if (state) { speechFn(that.model.text, true, {onend: that.events.utteranceOnEnd.fire}); } }; fluid.orator.selectionReader.bindSelectionEvents = function (that) { $(document).on("selectionchange", function (e) { if (that.model.enabled) { that.events.onSelectionChanged.fire(e); } }); }; fluid.orator.selectionReader.updateText = function (that, state) { if (state) { that.getSelectedText(); } else { that.applier.change("text", "", "ADD", "updateText"); } }; /** * Retrieves the text from the current selection * * @return {String} - the text from the current selection */ fluid.orator.selectionReader.getSelectedText = function () { return window.getSelection().toString(); }; fluid.orator.selectionReader.location = { TOP: 0, RIGHT: 1, BOTTOM: 2, LEFT: 3 }; /** * An object containing specified offset scales: "edge" and "pointer" offsets to account for the room needed for a * control element to be correctly positioned. * * @typedef {Object} OffsetScale * @property {Float} edge - The minimum distance between the button and the viewport's edges * @property {Float} pointer - The distance between the button and the coordinates the DOMRect refers too. This * provides space for an arrow to point from the button. */ /** * An object containing the sizes of the top and left margins. * * @typedef {Object} MarginInfo * @property {Float} top - The size of margin-top * @property {Float} left - The size of margin-left */ /** * Coordinates for absolutely positioning a DOM Element. * * @typedef {Object} ControlPosition * @property {Float} top - The `top` pixel coordinate relative to the top/left corner * @property {Float} left - The `left` pixel coordinate relative to the top/left corner * @property {Integer} location - For location constants see: fluid.orator.selectionReader.location */ /** * Returns a position object containing coordinates for absolutely positioning the play button * relative to a passed in rect. By default it will be placed above the rect unless there is a collision with the * top of the window. In which case it will be placed below. This will be captured in the "location" propertied, * and is specified by a constant (See: fluid.orator.selectionReader.location). * * In addition to collision detection with the top of the window, collision detection for the left and right edges * of the window are also taken into account. However, the position will not be flipped, but will be translated * slightly to ensure that the item being placed is displayed on screen. These calculations are facilitated through * an offsetScale object passed in. * * @param {DOMRect} rect - A DOMRect object, used to calculate placement against. Specifically, the "top", "bottom", * and "left" properties may be used for positioning. * @param {MarginInfo} margin - Margin sizes * @param {Float} fontSize - The base font to multiple the offset against * @param {OffsetScale} offsetScale - (Optional) an object containing specified offsets: "edge" and "pointer". * Offsets all default to 1 and are multiplied with the fontSize for determining * the final offset value. * * @return {ControlPosition} - An object containing the coordinates for positioning the play button. * It takes the form {top: Float, left: Float, location: Integer} * For location constants see: fluid.orator.selectionReader.location */ fluid.orator.selectionReader.calculatePosition = function (rect, margin, fontSize, offsetScale) { var edgeOffset = fontSize * (fluid.get(offsetScale, "edge") || 1); var pointerOffset = fontSize * (fluid.get(offsetScale, "pointer") || 1); var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; var position = { top: scrollTop - margin.top, left: Math.min( Math.max(rect.left + scrollLeft - margin.left, edgeOffset + scrollLeft), (document.documentElement.clientWidth + scrollLeft - margin.left - edgeOffset) ) }; if (rect.top < edgeOffset) { position.top = position.top + rect.bottom; position.location = fluid.orator.selectionReader.location.BOTTOM; } else { position.top = position.top + rect.top - pointerOffset; position.location = fluid.orator.selectionReader.location.TOP; } return position; }; fluid.orator.selectionReader.renderControlState = function (that, control) { var text = that.options.strings[that.model.play ? "stop" : "play"]; control.find(that.options.selectors.controlLabel).text(text); }; fluid.orator.selectionReader.renderControl = function (that, state) { if (state) { var selectionRange = window.getSelection().getRangeAt(0); var rect = selectionRange.getClientRects()[0]; var fontSize = parseFloat(that.container.css("font-size")); var margin = { top: parseFloat(that.container.css("margin-top")), left: parseFloat(that.container.css("margin-left")) }; var position = fluid.orator.selectionReader.calculatePosition(rect, margin, fontSize, that.options.offsetScale); var control = $(that.options.markup.control); control.addClass(that.options.styles.control); fluid.orator.selectionReader.renderControlState(that, control); control.css({ top: position.top, left: position.left }); var positionClass = that.options.styles[position.location === fluid.orator.selectionReader.location.TOP ? "above" : "below"]; control.addClass(positionClass); control.click(function () { // wrapped in an empty function so as not to pass along the jQuery event object that.events.onToggleControl.fire(); }); control.appendTo(that.container); // cleanup range selectionRange.detach(); } else { that.locate("control").remove(); } }; fluid.orator.selectionReader.togglePlay = function (that, state) { var newState = state || !that.model.play; that[newState ? "play" : "stop"](); }; })(jQuery, fluid_3_0_0);