diff --git a/.gitignore b/.gitignore index f9735ee7..3ccda18a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ typings *.user src/scripts/**/*_internal.* .vscode/tasks.json +.vscode/mcp.json +.claude/ +.mcp.json /.idea .editorconfig /OneNoteWebClipper/edgeextension \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a21eee78..aa83f65b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ { "editor.formatOnType": true, "editor.insertSpaces": false, - "editor.renderWhitespace": true, + "editor.renderWhitespace": "boundary", "files.exclude": { "**/.git": true, "**/.DS_Store": true, diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 357754f5..46fe340b 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -303,3 +303,21 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------- + +@mozilla/readability + +Copyright (c) 2010 Arc90 Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/docs/client-side-migration.md b/docs/client-side-migration.md new file mode 100644 index 00000000..6360edc5 --- /dev/null +++ b/docs/client-side-migration.md @@ -0,0 +1,302 @@ +# WebClipper Client-Side Migration + +## Overview + +This document tracks the experiment to remove server-side dependencies from the OneNote Web Clipper's content processing pipeline and replace them with client-side alternatives. The goal is a fully self-contained browser extension that does not rely on the OneNote augmentation/screenshot server APIs. + +## Server APIs Removed + +### 1. Augmentation API +- **Endpoint:** `onenote.com/onaugmentation/clipperextract/v1.0/` +- **Purpose:** Server-side article/recipe/product extraction using ML models +- **Replacement:** Mozilla Readability (`@mozilla/readability`, Apache 2.0 license) +- **Status:** Complete + +### 2. Full Page Screenshot API (DomEnhancer) +- **Endpoint:** `onenote.com/onaugmentation/clipperDomEnhancer/v1.0/` +- **Purpose:** Server-side Puppeteer rendering of page DOM into full-page screenshots +- **Replacement:** Client-side renderer window with scroll-capture and canvas stitching +- **Status:** Functional, with known issues (see below) + +--- + +## Change 1: Article Extraction with Readability.js + +### What Changed +- `augmentationHelper.ts` — Rewrote `augmentPage()` to use `new Readability(doc).parse()` locally instead of POSTing to the server API +- Removed `makeAugmentationRequest()` method entirely +- Removed imports: `HttpWithRetries`, `OneNoteApiUtils`, `Settings`, `Constants` (URL refs) +- Added metadata mapping: Readability's `title`, `excerpt`, `byline`, `siteName`, `publishedTime` are stored in `PageMetadata` + +### Why Readability.js +- Apache 2.0 license (compatible with WebClipper's MIT license; repo already has Apache 2.0 deps like pdfjs-dist) +- Well-maintained by Mozilla, used in Firefox Reader View +- Produces clean article HTML similar to what the server API returned + +### Other Related Changes +- `clipper.tsx` — Removed `UrlUtils.onWhitelistedDomain()` check that gated augmentation mode; FullPage is now the default clip mode +- `constants.ts` — Removed `augmentationApiUrl` constant +- `readability.d.ts` (new) — TypeScript type declarations for `@mozilla/readability` +- `package.json` — Added `@mozilla/readability` dependency +- `augmentationHelper_tests.ts` — Updated tests for new local implementation + +--- + +## Change 2: Full Page Screenshot with Renderer Window + +### Architecture + +The server-side approach used Puppeteer to render sanitized HTML and produce a full-page screenshot. The client-side replacement mirrors this: + +1. **Store HTML in `chrome.storage.session`** — The page's HTML content, base URL, and localized status text are written to session storage (avoids JSON serialization bottleneck with large payloads) +2. **Open a renderer popup window** — An extension page (`renderer.html`) is opened at the same position/size as the user's browser with `focused: true`. Content width is capped at 1024px (popup fits comfortably on most monitors); height is capped at 900px and floored at 600px. Zoom is forced to 100% via `chrome.tabs.setZoom`. Title bar shows localized "Clipping Page" status text +3. **Port-based communication** — The renderer page connects to the service worker via `chrome.runtime.connect({ name: "renderer" })`. Commands (loadContent, scroll) are exchanged over this port +4. **Renderer loads content** — Reads HTML from `chrome.storage.session`, strips ` - - -
-
-
- -
-
-
- - - - - - - - - - - - - diff --git a/src/images/article.svg b/src/images/article.svg index 291c28bb..181f2a11 100644 --- a/src/images/article.svg +++ b/src/images/article.svg @@ -1,8 +1,3 @@ - - - - - - - + + diff --git a/src/images/bookmark.svg b/src/images/bookmark.svg index bfca617b..1d60b64e 100644 --- a/src/images/bookmark.svg +++ b/src/images/bookmark.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/images/editorOptions/font_down.svg b/src/images/editorOptions/font_down.svg index bbf20b49..2429d307 100644 --- a/src/images/editorOptions/font_down.svg +++ b/src/images/editorOptions/font_down.svg @@ -1,3 +1,3 @@ - + diff --git a/src/images/editorOptions/font_up.svg b/src/images/editorOptions/font_up.svg index 9f022267..e4a8786a 100644 --- a/src/images/editorOptions/font_up.svg +++ b/src/images/editorOptions/font_up.svg @@ -1,3 +1,3 @@ - + diff --git a/src/images/editorOptions/highlight_tool_off.svg b/src/images/editorOptions/highlight_tool_off.svg index 4f7a9d89..55a43bdd 100644 --- a/src/images/editorOptions/highlight_tool_off.svg +++ b/src/images/editorOptions/highlight_tool_off.svg @@ -1,5 +1,5 @@ - - + + diff --git a/src/images/feedback_smiley.svg b/src/images/feedback_smiley.svg index 4eb43ce8..415bfd78 100644 --- a/src/images/feedback_smiley.svg +++ b/src/images/feedback_smiley.svg @@ -1,3 +1,3 @@ - + diff --git a/src/images/fullpage.svg b/src/images/fullpage.svg index 2528fc14..c0c78528 100644 --- a/src/images/fullpage.svg +++ b/src/images/fullpage.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/images/region.svg b/src/images/region.svg index ad519c1f..734f256a 100644 --- a/src/images/region.svg +++ b/src/images/region.svg @@ -1,19 +1,3 @@ - - - - - - - - - - - - - - - - - - + + diff --git a/src/pageNav.html b/src/pageNav.html deleted file mode 100644 index 34579ba6..00000000 --- a/src/pageNav.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - OneNote Web Clipper - - - - - -
- - - - - - - diff --git a/src/renderer.html b/src/renderer.html new file mode 100644 index 00000000..f5aeffc4 --- /dev/null +++ b/src/renderer.html @@ -0,0 +1,156 @@ + + + + + + + + +
+
+ OneNote +

OneNote Web Clipper

+

Sign in to clip this page to OneNote

+ + +
+
Signing in...
+
+
+
+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+ + OneNote Web Clipper +
+
+
+ +
+ + + + + +
+
+
+ + +
+
+
+ +
PDF too large to attach
+ +
+
+
+
+ + + + + +
+
+
+ +
+
+
+
    +
    +
    +
    +
    + + +
    +
    +
    + + +
    +

    + +
    +
    +
    + + +
    +

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + Sign out +
    + Feedback +
    +
    +
    + + + + diff --git a/src/scripts/clipperUI/animatedTooltip.tsx b/src/scripts/clipperUI/animatedTooltip.tsx deleted file mode 100644 index 0a8754f1..00000000 --- a/src/scripts/clipperUI/animatedTooltip.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import {Constants} from "../constants"; - -import {ComponentBase} from "./componentBase"; -import {Tooltip, TooltipProps} from "./tooltip"; - -import {AnimationState} from "./animations/animationState"; -import {AnimationStrategy} from "./animations/animationStrategy"; -import {ExpandFromRightAnimationStrategy} from "./animations/expandFromRightAnimationStrategy"; -import {SlidingHeightAnimationStrategy, NewHeightInfo} from "./animations/slidingHeightAnimationStrategy"; - -export interface AnimatedTooltipState { - uiExpanded: boolean; -} - -export interface AnimatedTooltipProps extends TooltipProps { - onAfterCollapse?: (tooltipElement: HTMLElement) => void; - onHeightChange?: (newHeightInfo: NewHeightInfo) => void; -} - -export class AnimatedTooltipClass extends ComponentBase { - private tooltipAnimationStrategy: AnimationStrategy; - private heightAnimationStrategy: AnimationStrategy; - - constructor(props: AnimatedTooltipProps) { - super(props); - this.tooltipAnimationStrategy = new ExpandFromRightAnimationStrategy({ - extShouldAnimateIn: () => { return this.state.uiExpanded; }, - extShouldAnimateOut: () => { return !this.state.uiExpanded; }, - onAfterAnimateOut: this.props.onAfterCollapse - }); - this.heightAnimationStrategy = new SlidingHeightAnimationStrategy(this.props.elementId, { - onAfterHeightAnimatorDraw: this.props.onHeightChange - }); - } - - getInitialState(): AnimatedTooltipState { - return { - uiExpanded: true - }; - } - - closeTooltip() { - this.setState({ uiExpanded: false }); - if (this.props.onCloseButtonHandler) { - this.props.onCloseButtonHandler(); - } - } - - onHeightAnimatorDraw(heightAnimator: HTMLElement) { - this.heightAnimationStrategy.animate(heightAnimator); - } - - onTooltipDraw(tooltipElement: HTMLElement) { - this.tooltipAnimationStrategy.animate(tooltipElement); - } - - render() { - // We have to make the renderablePanel undefined on the collapse for the vertical shrink animation to function correctly - let renderablePanel = ( -
    - {this.state.uiExpanded ? this.props.renderablePanel : undefined} -
    - ); - return ( - - ); - } -} - -let component = AnimatedTooltipClass.componentize(); -export {component as AnimatedTooltip}; diff --git a/src/scripts/clipperUI/animations/animationHelper.ts b/src/scripts/clipperUI/animations/animationHelper.ts deleted file mode 100644 index bf7a41e5..00000000 --- a/src/scripts/clipperUI/animations/animationHelper.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -export class AnimationHelper { - public static stopAnimationsThen(el: HTMLElement, callback: () => void) { - Velocity.animate(el, "stop", true as any); - setTimeout(callback, 1); - } -} diff --git a/src/scripts/clipperUI/animations/animationState.ts b/src/scripts/clipperUI/animations/animationState.ts deleted file mode 100644 index 9d51de7a..00000000 --- a/src/scripts/clipperUI/animations/animationState.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum AnimationState { - // Used for when elements transition in and out (i.e., the next element is replacing the first) - GoingIn, // Element currently animating in - GoingOut, // Element currently animating out - In, // No animation, element is in - Out, // No animation, element is out - - // Used for when the same element is transitioning from one state to the next (e.g., changing dimensions) - Transitioning, - Stopped -} diff --git a/src/scripts/clipperUI/animations/animationStrategy.ts b/src/scripts/clipperUI/animations/animationStrategy.ts deleted file mode 100644 index c535858a..00000000 --- a/src/scripts/clipperUI/animations/animationStrategy.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {SmartValue} from "../../communicator/smartValue"; - -import {AnimationHelper} from "./animationHelper"; -import {AnimationState} from "./animationState"; - -/** - * Represents a strategy object for handling the animations for the given element. - * Child classes should only have to implement the animation itself, as well as any - * additional functional requirements, such as callbacks. - */ -export abstract class AnimationStrategy { - protected animationDuration: number; - - private animationState: SmartValue; - - constructor(animationDuration: number, animationState?: SmartValue) { - this.animationDuration = animationDuration; - this.animationState = animationState || new SmartValue(AnimationState.Stopped); - } - - protected abstract doAnimate(el: HTMLElement): Promise; - - public getAnimationState(): AnimationState { - return this.animationState.get(); - } - - public setAnimationState(animationState: AnimationState) { - this.animationState.set(animationState); - } - - public animate(el: HTMLElement) { - AnimationHelper.stopAnimationsThen(el, () => { - this.setAnimationState(AnimationState.Transitioning); - this.doAnimate(el).then(() => { - this.setAnimationState(AnimationState.Stopped); - }); - }); - } -} diff --git a/src/scripts/clipperUI/animations/expandFromRightAnimationStrategy.ts b/src/scripts/clipperUI/animations/expandFromRightAnimationStrategy.ts deleted file mode 100644 index de929e72..00000000 --- a/src/scripts/clipperUI/animations/expandFromRightAnimationStrategy.ts +++ /dev/null @@ -1,81 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -import {Constants} from "../../constants"; - -import {AnimationState} from "./animationState"; -import {AnimationStrategy} from "./animationStrategy"; -import {TransitioningAnimationStrategy, TransitioningAnimationStrategyOptions} from "./transitioningAnimationStrategy"; - -export interface ExpandFromRightAnimationStrategyOptions extends TransitioningAnimationStrategyOptions { - onAnimateInExpand?: (el: HTMLElement) => void; -} - -/** - * Represents an animation where elements transition in by expanding from the top right. - * When transitioning out, the opposite happens. - */ -export class ExpandFromRightAnimationStrategy extends TransitioningAnimationStrategy { - private animationTimeout: number; - private animationTimeoutId: number; - private reverseChildAnimations: boolean; - - constructor(options: ExpandFromRightAnimationStrategyOptions) { - super(500 /* animationDuration */, options); - this.animationTimeout = 100; - this.reverseChildAnimations = true; - } - - protected doAnimateIn(el: HTMLElement) { - return new Promise((resolve) => { - this.reverseChildAnimations = true; - - clearTimeout(this.animationTimeoutId); - if (this.options.onAnimateInExpand) { - this.animationTimeoutId = setTimeout(() => { - this.options.onAnimateInExpand(el); - }, this.animationTimeout); - } - - Velocity.animate(el, { - opacity: 1, - right: 20, - width: Constants.Styles.clipperUiWidth - }, { - complete: () => { - // The first transition is reversed; once it is done, do the normal transitions - this.reverseChildAnimations = false; - resolve(); - }, - duration: this.animationDuration, - easing: "easeOutExpo" - }); - }); - } - - protected doAnimateOut(el: HTMLElement) { - return new Promise((resolve) => { - clearTimeout(this.animationTimeoutId); - this.animationTimeoutId = setTimeout(() => { - Velocity.animate(el, { - opacity: 0, - right: 0, - width: 0 - }, { - complete: () => { - resolve(); - }, - duration: this.animationDuration, - easing: "easeOutExpo" - }); - }, this.animationTimeout); - }); - } - - protected intShouldAnimateIn(el: HTMLElement): boolean { - return this.getAnimationState() === AnimationState.GoingOut || this.getAnimationState() === AnimationState.Out; - } - - protected intShouldAnimateOut(el: HTMLElement): boolean { - return this.getAnimationState() === AnimationState.GoingIn || this.getAnimationState() === AnimationState.In; - } -} diff --git a/src/scripts/clipperUI/animations/fadeInAnimationStrategy.ts b/src/scripts/clipperUI/animations/fadeInAnimationStrategy.ts deleted file mode 100644 index 3cb3852e..00000000 --- a/src/scripts/clipperUI/animations/fadeInAnimationStrategy.ts +++ /dev/null @@ -1,52 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -import {AnimationState} from "./animationState"; -import {TransitioningAnimationStrategy, TransitioningAnimationStrategyOptions} from "./transitioningAnimationStrategy"; - -/** - * Represents an animation where elements fade in. - * When transitioning out, elements fade away. - */ -export class FadeInAnimationStrategy extends TransitioningAnimationStrategy { - constructor(options?: TransitioningAnimationStrategyOptions) { - super(200 /* animationDuration */, options); - } - - protected doAnimateIn(el: HTMLElement): Promise { - return new Promise((resolve) => { - el.style.opacity = "0"; - - Velocity.animate(el, { - opacity: 1 - }, { - complete: () => { - resolve(); - }, - duration: this.animationDuration, - easing: "easeInOutQuad" - }); - }); - } - - protected doAnimateOut(el: HTMLElement): Promise { - return new Promise((resolve) => { - Velocity.animate(el, { - opacity: 0 - }, { - complete: () => { - resolve(); - }, - duration: this.animationDuration, - easing: "easeInOutQuad" - }); - }); - } - - protected intShouldAnimateIn(el: HTMLElement): boolean { - return this.getAnimationState() === AnimationState.Out; - } - - protected intShouldAnimateOut(el: HTMLElement): boolean { - return this.getAnimationState() === AnimationState.In; - } -} diff --git a/src/scripts/clipperUI/animations/slideContentInFromTopAnimationStrategy.ts b/src/scripts/clipperUI/animations/slideContentInFromTopAnimationStrategy.ts deleted file mode 100644 index 336378e3..00000000 --- a/src/scripts/clipperUI/animations/slideContentInFromTopAnimationStrategy.ts +++ /dev/null @@ -1,113 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -import {SmartValue} from "../../communicator/smartValue"; - -import {AnimationState} from "./animationState"; -import {TransitioningAnimationStrategy, TransitioningAnimationStrategyOptions} from "./transitioningAnimationStrategy"; - -export interface SlideContentInFromTopAnimationStrategyOptions extends TransitioningAnimationStrategyOptions { - currentAnimationState: SmartValue; - contentToAnimate: ContentToAnimate[]; -} - -export interface ContentToAnimate { - cssSelector: string; - animateInOptions: AnimateInOptions; -} - -export interface AnimateInOptions { - slideDownDeltas: number[]; - delaysInMs: number[]; -} - -/** - * Represents an animation where content fades in and slides downward into its position within the parent element provided. - * When animating out, content will fade out and slide upwards. - */ -export class SlideContentInFromTopAnimationStrategy extends TransitioningAnimationStrategy { - private animateInDuration: number; - private animateOutDuration: number; - private animateOutSlideUpDelta: number; - private contentToAnimate: ContentToAnimate[]; - - constructor(options: SlideContentInFromTopAnimationStrategyOptions) { - super(undefined /* animationDuration */, options, options.currentAnimationState); - - this.animateInDuration = 367; - this.animateOutDuration = 267; - this.animateOutSlideUpDelta = 23; - this.contentToAnimate = options.contentToAnimate; - } - - protected doAnimateIn(parentEl: HTMLElement): Promise { - return new Promise((resolve) => { - for (let contentIndex = 0; contentIndex < this.contentToAnimate.length; contentIndex++) { - let content = this.contentToAnimate[contentIndex]; - let animatables = parentEl.querySelectorAll(content.cssSelector) as NodeListOf; - - for (let animatableIndex = 0; animatableIndex < animatables.length; animatableIndex++) { - let isLastElementToAnimate: boolean = (contentIndex === this.contentToAnimate.length - 1) && (animatableIndex === animatables.length - 1); - - this.animateElementIn(animatables[animatableIndex], content.animateInOptions.slideDownDeltas[animatableIndex], content.animateInOptions.delaysInMs[animatableIndex], isLastElementToAnimate) - .then((lastElementFinishedAnimating) => { - if (lastElementFinishedAnimating) { - resolve(); - } - }); - } - } - }); - } - - protected doAnimateOut(parentEl: HTMLElement): Promise { - return new Promise((resolve) => { - let childrenSelectors: string = this.contentToAnimate.map((content) => { return content.cssSelector; }).join(", "); - let animatables: NodeListOf = parentEl.querySelectorAll(childrenSelectors) as NodeListOf; - - Velocity.animate(animatables, { - top: -(this.animateOutSlideUpDelta), - opacity: [0, "linear"] - }, { - complete: () => { - resolve(); - }, - duration: this.animateOutDuration, - easing: [1, 0, 1, 1] - }); - }); - } - - protected intShouldAnimateIn(el: HTMLElement): boolean { - return this.getAnimationState() === AnimationState.Out; - } - - protected intShouldAnimateOut(el: HTMLElement): boolean { - return this.getAnimationState() === AnimationState.In; - } - - /** - * Apply animate-in styling to a single element of content - */ - private animateElementIn(elem: HTMLElement, slideDownDelta: number, delayInMs: number, isLastElementToAnimate: boolean): Promise { - return new Promise((resolve) => { - elem.style.top = -(slideDownDelta) + "px"; - elem.style.opacity = "0"; - - Velocity.animate(elem, { - top: 0, - opacity: [1, "linear"] - }, { - complete: () => { - if (isLastElementToAnimate) { - resolve(true); - } else { - resolve(false); - } - }, - delay: delayInMs, - duration: this.animateInDuration, - easing: [0, 0, 0, 1] - }); - }); - } -} diff --git a/src/scripts/clipperUI/animations/slidingHeightAnimationStrategy.ts b/src/scripts/clipperUI/animations/slidingHeightAnimationStrategy.ts deleted file mode 100644 index a36053ac..00000000 --- a/src/scripts/clipperUI/animations/slidingHeightAnimationStrategy.ts +++ /dev/null @@ -1,87 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -import {Constants} from "../../constants"; - -import {AnimationStrategy} from "./animationStrategy"; - -export interface SlidingHeightAnimationStrategyOptions { - onBeforeHeightAnimatorDraw?: (newHeightInfo: NewHeightInfo) => void; - onAfterHeightAnimatorDraw?: (newHeightInfo: NewHeightInfo) => void; -} - -export interface NewHeightInfo { - actualPanelHeight: number; - newContainerHeight: number; - newPanelHeight: number; -} - -/** - * Represents an animation where element are able to adjust their height by performing - * a 'slide' animation. - */ -export class SlidingHeightAnimationStrategy extends AnimationStrategy { - private containerId: string; - private options: SlidingHeightAnimationStrategyOptions; - - constructor(containerId: string, options?: SlidingHeightAnimationStrategyOptions) { - super(200 /* animationDuration */); - this.containerId = containerId; - this.options = options; - } - - protected doAnimate(el: HTMLElement): Promise { - return new Promise((resolve) => { - let container = document.getElementById(this.containerId); - let newHeightInfo: NewHeightInfo = this.getContainerTrueHeight(container, el); - - if (this.options.onBeforeHeightAnimatorDraw) { - this.options.onBeforeHeightAnimatorDraw(newHeightInfo); - } - - // If there's nothing to animate then call it good. - if (newHeightInfo.actualPanelHeight === newHeightInfo.newPanelHeight) { - resolve(); - return; - } - - let delayResize = newHeightInfo.actualPanelHeight > newHeightInfo.newPanelHeight; - if (!delayResize && this.options.onAfterHeightAnimatorDraw) { - this.options.onAfterHeightAnimatorDraw(newHeightInfo); - } - - Velocity.animate(el, { - maxHeight: newHeightInfo.newPanelHeight, - minHeight: newHeightInfo.newPanelHeight - }, { - complete: () => { - if (delayResize && this.options.onAfterHeightAnimatorDraw) { - this.options.onAfterHeightAnimatorDraw(newHeightInfo); - } - resolve(); - }, - duration: this.animationDuration, - easing: "easeOutQuad" - }); - }); - } - - private getContainerTrueHeight(container: HTMLElement, heightAnimator: HTMLElement): NewHeightInfo { - let actualPanelHeight = parseFloat(heightAnimator.style.maxHeight.replace("px", "")); - if (isNaN(actualPanelHeight)) { - actualPanelHeight = 0; - } - - // Temporarily remove these so we can calculate the destination heights. - heightAnimator.style.maxHeight = ""; - heightAnimator.style.minHeight = ""; - - // At this point the new container size has been set, so we need to grab it so that we know where to animate to. - let newContainerHeight = container ? container.offsetHeight : 0; - let newPanelHeight = heightAnimator.offsetHeight; - - // Now set to the height back to what it was so that there is something to animate from. - heightAnimator.style.maxHeight = actualPanelHeight + "px"; - heightAnimator.style.minHeight = actualPanelHeight + "px"; - return { actualPanelHeight: actualPanelHeight, newContainerHeight: newContainerHeight, newPanelHeight: newPanelHeight }; - } -} diff --git a/src/scripts/clipperUI/animations/transitioningAnimationStrategy.ts b/src/scripts/clipperUI/animations/transitioningAnimationStrategy.ts deleted file mode 100644 index db358610..00000000 --- a/src/scripts/clipperUI/animations/transitioningAnimationStrategy.ts +++ /dev/null @@ -1,98 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -import {ObjectUtils} from "../../objectUtils"; - -import {SmartValue} from "../../communicator/smartValue"; - -import {AnimationHelper} from "./animationHelper"; -import {AnimationState} from "./animationState"; -import {AnimationStrategy} from "./animationStrategy"; - -export interface TransitioningAnimationStrategyOptions { - extShouldAnimateIn: () => boolean; - extShouldAnimateOut: () => boolean; - - onBeforeAnimateOut?: (el: HTMLElement) => void; - onBeforeAnimateIn?: (el: HTMLElement) => void; - - onAfterAnimateOut?: (el: HTMLElement) => void; - onAfterAnimateIn?: (el: HTMLElement) => void; -} - -/** - * Represents the family of animations where elements are able to toggle their visibility completely. - * - * Assumes that the decision to animate out vs animate in is relient on both external and internal - * factors. Implementing classes will implement the internal factors, but can leave room for external - * factors to weigh in on the decision as well. - */ -export abstract class TransitioningAnimationStrategy extends AnimationStrategy { - protected options: TOptions; - - constructor(animationDuration: number, options: TOptions, animationState?: SmartValue) { - animationState = animationState || new SmartValue(); - - if (ObjectUtils.isNullOrUndefined(animationState.get())) { - animationState.set(AnimationState.Out); - } - - super(animationDuration, animationState); - this.options = options; - } - - protected abstract doAnimateIn(el: HTMLElement): Promise; - protected abstract doAnimateOut(el: HTMLElement): Promise; - protected abstract intShouldAnimateIn(el: HTMLElement): boolean; - protected abstract intShouldAnimateOut(el: HTMLElement): boolean; - - // Override - public animate(el: HTMLElement) { - // We only stop animations when we actually animate, so we call stopAnimationsThen - // in the animateIn and animateOut functions instead of here - this.doAnimate(el); - } - - protected doAnimate(el: HTMLElement): Promise { - if (this.options.extShouldAnimateIn() && this.intShouldAnimateIn(el)) { - return this.animateIn(el); - } else if (this.options.extShouldAnimateOut() && this.intShouldAnimateOut(el)) { - return this.animateOut(el); - } - } - - private animateIn(el: HTMLElement): Promise { - return new Promise((resolve) => { - AnimationHelper.stopAnimationsThen(el, () => { - if (this.options.onBeforeAnimateIn) { - this.options.onBeforeAnimateIn(el); - } - this.setAnimationState(AnimationState.GoingIn); - this.doAnimateIn(el).then(() => { - this.setAnimationState(AnimationState.In); - if (this.options.onAfterAnimateIn) { - this.options.onAfterAnimateIn(el); - } - resolve(); - }); - }); - }); - } - - private animateOut(el: HTMLElement): Promise { - return new Promise((resolve) => { - AnimationHelper.stopAnimationsThen(el, () => { - if (this.options.onBeforeAnimateOut) { - this.options.onBeforeAnimateOut(el); - } - this.setAnimationState(AnimationState.GoingOut); - this.doAnimateOut(el).then(() => { - this.setAnimationState(AnimationState.Out); - if (this.options.onAfterAnimateOut) { - this.options.onAfterAnimateOut(el); - } - resolve(); - }); - }); - }); - } -} diff --git a/src/scripts/clipperUI/clipper.tsx b/src/scripts/clipperUI/clipper.tsx deleted file mode 100644 index 57511271..00000000 --- a/src/scripts/clipperUI/clipper.tsx +++ /dev/null @@ -1,815 +0,0 @@ -import {AuthType, UpdateReason, UserInfo} from "../userInfo"; -import {BrowserUtils} from "../browserUtils"; -import {ClientInfo} from "../clientInfo"; -import {Constants} from "../constants"; -import {ObjectUtils} from "../objectUtils"; - -import {PageInfo} from "../pageInfo"; -import {Polyfills} from "../polyfills"; -import {PreviewGlobalInfo} from "../previewInfo"; -import {TooltipType} from "./tooltipType"; -import {UrlUtils} from "../urlUtils"; - -import {Communicator} from "../communicator/communicator"; -import {IFrameMessageHandler} from "../communicator/iframeMessageHandler"; -import {InlineMessageHandler} from "../communicator/inlineMessageHandler"; -import {SmartValue} from "../communicator/smartValue"; - -import {AugmentationHelper} from "../contentCapture/augmentationHelper"; -import {BookmarkError, BookmarkHelper, BookmarkResult} from "../contentCapture/bookmarkHelper"; -import {FullPageScreenshotHelper} from "../contentCapture/fullPageScreenshotHelper"; -import {PdfScreenshotHelper, PdfScreenshotResult} from "../contentCapture/pdfScreenshotHelper"; - -import {DomUtils} from "../domParsers/domUtils"; -import {VideoUtils} from "../domParsers/videoUtils"; - -import {ClipperInjectOptions} from "../extensions/clipperInject"; -import {InvokeOptions, InvokeMode} from "../extensions/invokeOptions"; - -import {InlineExtension} from "../extensions/bookmarklet/inlineExtension"; - -import {CachedHttp, TimeStampedData} from "../http/cachedHttp"; - -import {Localization} from "../localization/localization"; - -import * as Log from "../logging/log"; -import {CommunicatorLoggerPure} from "../logging/communicatorLoggerPure"; - -import {OneNoteSaveableFactory} from "../saveToOneNote/oneNoteSaveableFactory"; -import {SaveToOneNote, SaveToOneNoteOptions} from "../saveToOneNote/saveToOneNote"; -import {SaveToOneNoteLogger} from "../saveToOneNote/saveToOneNoteLogger"; - -import {ClipperStorageKeys} from "../storage/clipperStorageKeys"; - -import {ClipMode} from "./clipMode"; -import {Clipper} from "./frontEndGlobals"; -import {ClipperState} from "./clipperState"; -import {ClipperStateUtilities} from "./clipperStateUtilities"; -import {ComponentBase} from "./componentBase"; -import {MainController} from "./mainController"; -import {OneNoteApiUtils} from "./oneNoteApiUtils"; -import {PreviewViewer} from "./previewViewer"; -import {RatingsHelper} from "./ratingsHelper"; -import {RegionSelector} from "./regionSelector"; -import {Status} from "./status"; - -import * as _ from "lodash"; -import { WebExtension } from "../extensions/webExtensionBase/webExtension"; -import { ClientType } from "../clientType"; - -class ClipperClass extends ComponentBase { - private isFullScreen = new SmartValue(false); - - constructor(props: {}) { - super(props); - - this.initializeCommunicators(); - this.initializeSmartValues(); - } - - getInitialState(): ClipperState { - return { - uiExpanded: true, - currentMode: new SmartValue(this.getDefaultClipMode()), - - userResult: { status: Status.NotStarted } , - fullPageResult: { status: Status.NotStarted }, - pdfResult: { data: new SmartValue(), status: Status.NotStarted }, - regionResult: { status: Status.NotStarted, data: [] }, - augmentationResult: { status: Status.NotStarted }, - oneNoteApiResult: { status: Status.NotStarted }, - bookmarkResult: { status: Status.NotStarted }, - - setState: (partialState: ClipperState) => { - this.setState(partialState); - this.isFullScreen.set(ClipperClass.shouldShowPreviewViewer(this.state) || ClipperClass.shouldShowRegionSelector(this.state)); - }, - - previewGlobalInfo: { - annotation: "", - fontSize: parseInt(Localization.getLocalizedString("WebClipper.FontSize.Preview.SansSerifDefault"), 10 /* radix */), - highlighterEnabled: false, - serif: false - }, - augmentationPreviewInfo: {}, - selectionPreviewInfo: {}, - pdfPreviewInfo: { - allPages: true, - isLocalFileAndNotAllowed: true, - selectedPageRange: "", - shouldAttachPdf: false, - shouldDistributePages: false, - shouldShowPopover: false - }, - clipSaveStatus: { - numItemsTotal: undefined, - numItemsCompleted: undefined - }, - - reset: () => { - this.state.setState(this.getResetState()); - } - }; - } - - private getResetState(): ClipperState { - return { - currentMode: this.state.currentMode.set(this.getDefaultClipMode()), - oneNoteApiResult: { status: Status.NotStarted } - }; - } - - private initializeInjectCommunicator(pageInfo: SmartValue, clientInfo: SmartValue) { - // Clear the inject no-op tracker - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.noOpTracker, (trackerStartTime: number) => { - let clearNoOpTrackerEvent = new Log.Event.BaseEvent(Log.Event.Label.ClearNoOpTracker); - clearNoOpTrackerEvent.setCustomProperty(Log.PropertyName.Custom.TimeToClearNoOpTracker, new Date().getTime() - trackerStartTime); - clearNoOpTrackerEvent.setCustomProperty(Log.PropertyName.Custom.Channel, Constants.CommunicationChannels.injectedAndUi); - Clipper.logger.logEvent(clearNoOpTrackerEvent); - - return Promise.resolve(); - }); - - // Register functions for Inject - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.showRefreshClipperMessage, (errorMessage: string) => { - if (!this.state.badState) { - Clipper.logger.logFailure(Log.Failure.Label.OrphanedWebClippersDueToExtensionRefresh, Log.Failure.Type.Expected, - { error: errorMessage }); - this.state.setState({ - badState: true - }); - } - }); - - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.toggleClipper, () => { - this.state.setState({ uiExpanded: !this.state.uiExpanded }); - }); - - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.onSpaNavigate, () => { - // This could have been called when the UI is already toggled off - if (this.state.uiExpanded) { - let hideClipperDueToSpaNavigateEvent = new Log.Event.BaseEvent(Log.Event.Label.HideClipperDueToSpaNavigate); - Clipper.logger.logEvent(hideClipperDueToSpaNavigateEvent); - this.state.setState({ uiExpanded: false }); - } - }); - - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.setInvokeOptions, (options: InvokeOptions) => { - this.setInvokeOptions(options); - }); - - // Register smartValues for Inject - Clipper.getInjectCommunicator().broadcastAcrossCommunicator(this.isFullScreen, Constants.SmartValueKeys.isFullScreen); - - Clipper.getInjectCommunicator().subscribeAcrossCommunicator(pageInfo, Constants.SmartValueKeys.pageInfo, (updatedPageInfo: PageInfo) => { - if (updatedPageInfo) { - let newPreviewGlobalInfo = _.extend(this.state.previewGlobalInfo, { - previewTitleText: updatedPageInfo.contentTitle - } as PreviewGlobalInfo); - - this.state.setState({ - pageInfo: updatedPageInfo, - previewGlobalInfo: newPreviewGlobalInfo - }); - - this.capturePdfScreenshotContent(); - this.captureFullPageScreenshotContent(); - this.captureAugmentedContent(); - this.captureBookmarkContent(); - - Clipper.logger.setContextProperty(Log.Context.Custom.ContentType, OneNoteApi.ContentType[updatedPageInfo.contentType]); - } - }); - - Clipper.getInjectCommunicator().setErrorHandler((e: Error) => { - Log.ErrorUtils.handleCommunicatorError(Constants.CommunicationChannels.injectedAndUi, e, clientInfo); - }); - } - - private capturePdfScreenshotContent() { - // We don't capture anything. If the type is not EnhancedUrl, the mode will never show. - if (this.state.pageInfo.contentType !== OneNoteApi.ContentType.EnhancedUrl) { - return; - } - - // The PDF isn't going to change on the same url, so we avoid multiple GETs in the same page - if (this.state.pdfResult.status === Status.NotStarted) { - // If network file, send XHR, get bytes back, convert to PDFDocumentProxy - // If local file, get bytes back, convert to PDFDocumentProxy - this.state.setState({ pdfResult: { data: new SmartValue(undefined), status: Status.InProgress } }); - this.getPdfScreenshotResultFromRawUrl(this.state.pageInfo.rawUrl) - .then((pdfScreenshotResult: PdfScreenshotResult) => { - this.state.pdfResult.data.set(pdfScreenshotResult); - this.state.setState({ - pdfResult: { - data: this.state.pdfResult.data, - status: Status.Succeeded - } - }); - }) - .catch(() => { - this.state.pdfResult.data.set({ - failureMessage: Localization.getLocalizedString("WebClipper.Preview.FullPageModeGenericError") - }); - this.state.setState({ - pdfResult: { - data: this.state.pdfResult.data, - status: Status.Failed - } - }); - // The clip action might be waiting on the result, so do this to consistently trigger its callback - this.state.pdfResult.data.forceUpdate(); - }); - } - } - - private getPdfScreenshotResultFromRawUrl(rawUrl: string): Promise { - if (rawUrl.indexOf("file:///") === 0) { - return PdfScreenshotHelper.getLocalPdfData(rawUrl); - } else { - return PdfScreenshotHelper.getPdfData(rawUrl); - } - } - - private captureFullPageScreenshotContent() { - if (this.state.pageInfo.contentType === OneNoteApi.ContentType.EnhancedUrl) { - this.state.setState({ - fullPageResult: { - data: { - failureMessage: Localization.getLocalizedString("WebClipper.Preview.NoContentFound") - }, - status: Status.Failed - } - }); - } else { - this.state.setState({ fullPageResult: { status: Status.InProgress } }); - - FullPageScreenshotHelper.getFullPageScreenshot(this.state.pageInfo.contentData).then((result) => { - this.state.setState({ fullPageResult: { data: result, status: Status.Succeeded } }); - }, () => { - this.state.setState({ - fullPageResult: { - data: { - failureMessage: Localization.getLocalizedString("WebClipper.Preview.NoFullPageScreenshotFound") - }, - status: Status.Failed - } - }); - }); - } - } - - private captureAugmentedContent() { - if (this.state.pageInfo.contentType === OneNoteApi.ContentType.EnhancedUrl) { - this.state.setState({ - augmentationResult: { - data: { - failureMessage: Localization.getLocalizedString("WebClipper.Preview.NoContentFound") - }, - status: Status.Failed - } - }); - } else { - this.state.setState({ augmentationResult: { status: Status.InProgress } }); - - let pageInfo = this.state.pageInfo; - - let augmentationUrl = pageInfo.canonicalUrl; - if (pageInfo.rawUrl.indexOf("youtube.com") > -1 || pageInfo.rawUrl.indexOf("vimeo.com") > -1) { - augmentationUrl = pageInfo.rawUrl; - } - - AugmentationHelper.augmentPage(augmentationUrl, pageInfo.contentLocale, pageInfo.contentData).then((result) => { - result.ContentInHtml = DomUtils.cleanHtml(result.ContentInHtml); - this.state.setState({ - augmentationResult: { data: result, status: Status.Succeeded }, - augmentationPreviewInfo: { previewBodyHtml: result.ContentInHtml } - }); - }, () => { - this.state.setState({ - augmentationResult: { - data: { - failureMessage: Localization.getLocalizedString("WebClipper.Preview.AugmentationModeGenericError") - }, - status: Status.Failed - } - }); - }); - } - } - - private captureBookmarkContent() { - this.state.setState({ bookmarkResult: { status: Status.InProgress } }); - - let pageInfo = this.state.pageInfo; - let pageUrl = pageInfo.rawUrl; - let pageTitle = pageInfo.contentTitle; - - let doc = DomUtils.getDocumentFromDomString(pageInfo.contentData); - - let metadataElements: Element[] = BookmarkHelper.getElementsByTagName(doc, BookmarkHelper.metadataTagNames); - - let imageElements: HTMLImageElement[] = BookmarkHelper.getElementsByTagName(doc.body, [DomUtils.tags.img]) as HTMLImageElement[]; - - // because we are cleaning the document in order to get the cleanest text possible in our description, - // make sure this is the last operation being performed on the document - let textElements: Text[] = []; - try { - textElements = BookmarkHelper.getNonWhiteSpaceTextElements(doc, true /* cleanDoc */); - } catch (e) { - // IE11 has a weird issue where walk.nextNode() returns a non-descript exception, so we can't use our fallback logic - } - - BookmarkHelper.bookmarkPage(pageUrl, pageTitle, metadataElements, true /* allowFallback */, imageElements, textElements).then((result: BookmarkResult) => { - this.state.setState({ - bookmarkResult: { data: result, status: Status.Succeeded } - }); - }, (error: BookmarkError) => { - this.state.setState({ - bookmarkResult: { - data: { - url: error.url, - title: pageTitle, - description: error.description, - thumbnailSrc: error.thumbnailSrc, - failureMessage: Localization.getLocalizedString("WebClipper.Preview.BookmarkModeGenericError") - }, - status: Status.Failed - } - }); - }); - } - - private setInvokeOptions(options: InvokeOptions) { - this.setState({ invokeOptions: options }); - - // This needs to happen after the invokeOptions set as it is reliant on that order - this.setState({ currentMode: this.state.currentMode.set(this.getDefaultClipMode()) }); - - // We assume that invokeDataForMode is always a non-undefined value where it makes sense - // and that it's the background's responsibility to ensure that is the case - switch (options.invokeMode) { - case InvokeMode.ContextImage: - // invokeDataForMode is the src url - this.setState({ - regionResult: { - data: [options.invokeDataForMode], - status: Status.Succeeded - } - }); - break; - case InvokeMode.ContextTextSelection: - // invokeDataForMode is scrubbed selected html as a string - this.state.setState({ - selectionPreviewInfo: { - previewBodyHtml: options.invokeDataForMode - } - }); - break; - default: - break; - } - } - - private initializeExtensionCommunicator(clientInfo: SmartValue) { - // Clear the extension no-op tracker - Clipper.getExtensionCommunicator().registerFunction(Constants.FunctionKeys.noOpTracker, (trackerStartTime: number) => { - let clearNoOpTrackerEvent = new Log.Event.BaseEvent(Log.Event.Label.ClearNoOpTracker); - clearNoOpTrackerEvent.setCustomProperty(Log.PropertyName.Custom.TimeToClearNoOpTracker, new Date().getTime() - trackerStartTime); - clearNoOpTrackerEvent.setCustomProperty(Log.PropertyName.Custom.Channel, Constants.CommunicationChannels.extensionAndUi); - Clipper.logger.logEvent(clearNoOpTrackerEvent); - - return Promise.resolve(); - }); - - Clipper.getExtensionCommunicator().registerFunction(Constants.FunctionKeys.createHiddenIFrame, (url: string) => { - BrowserUtils.appendHiddenIframeToDocument(url); - }); - - let userInfoUpdateCb = (updatedUser: UserInfo) => { - if (updatedUser) { - let userInfoUpdatedEvent = new Log.Event.BaseEvent(Log.Event.Label.UserInfoUpdated); - userInfoUpdatedEvent.setCustomProperty(Log.PropertyName.Custom.UserUpdateReason, UpdateReason[updatedUser.updateReason]); - userInfoUpdatedEvent.setCustomProperty(Log.PropertyName.Custom.LastUpdated, new Date(updatedUser.lastUpdated).toUTCString()); - Clipper.logger.logEvent(userInfoUpdatedEvent); - } - - if (updatedUser && updatedUser.user) { - let timeStampedData: TimeStampedData = { - data: updatedUser.user, - lastUpdated: updatedUser.lastUpdated - }; - - // The user SV should never be set with expired user information - let tokenHasExpiredForLoggedInUser = CachedHttp.valueHasExpired(timeStampedData, (updatedUser.user.accessTokenExpiration * 1000) - 180000); - if (tokenHasExpiredForLoggedInUser) { - Clipper.logger.logFailure(Log.Failure.Label.UserSetWithInvalidExpiredData, Log.Failure.Type.Unexpected); - } - - this.state.setState({ userResult: { status: Status.Succeeded, data: updatedUser } }); - Clipper.logger.setContextProperty(Log.Context.Custom.AuthType, updatedUser.user.authType); - Clipper.logger.setContextProperty(Log.Context.Custom.UserInfoId, updatedUser.user.cid); - } else { - this.state.setState({ userResult: { status: Status.Failed, data: updatedUser } }); - } - }; - - this.state.setState({ userResult: { status: Status.InProgress } }); - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.getInitialUser, { - callback: (freshInitialUser: UserInfo) => { - if (freshInitialUser && freshInitialUser.user) { - Clipper.logger.logUserFunnel(Log.Funnel.Label.AuthAlreadySignedIn); - } else if (!freshInitialUser) { - userInfoUpdateCb(freshInitialUser); - } - Clipper.getExtensionCommunicator().subscribeAcrossCommunicator(new SmartValue(), Constants.SmartValueKeys.user, (updatedUser: UserInfo) => { - userInfoUpdateCb(updatedUser); - }); - } - }); - - this.state.setState({ fetchLocStringStatus: Status.InProgress }); - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.clipperStrings, { - callback: (data: Object) => { - if (data) { - Localization.setLocalizedStrings(data); - } - this.state.setState({ fetchLocStringStatus: Status.Succeeded }); - } - }); - - Clipper.getExtensionCommunicator().registerFunction(Constants.FunctionKeys.extensionNotAllowedToAccessLocalFiles, () => { - // We only want to log one time per session - if (this.state.pdfPreviewInfo.isLocalFileAndNotAllowed) { - Clipper.logger.logEvent(new Log.Event.BaseEvent(Log.Event.Label.LocalFilesNotAllowedPanelShown)); - } - - _.assign(_.extend(this.state.pdfPreviewInfo, { - isLocalFileAndNotAllowed: false - }), this.state.setState); - }); - - Clipper.getExtensionCommunicator().subscribeAcrossCommunicator(clientInfo, Constants.SmartValueKeys.clientInfo, (updatedClientInfo: ClientInfo) => { - if (updatedClientInfo) { - this.state.setState({ - clientInfo: updatedClientInfo - }); - } - }); - - Clipper.getExtensionCommunicator().setErrorHandler((e: Error) => { - Log.ErrorUtils.handleCommunicatorError(Constants.CommunicationChannels.extensionAndUi, e, clientInfo); - }); - } - - private initializeCommunicators() { - let pageInfo = new SmartValue(); - let clientInfo = new SmartValue(); - - Clipper.setInjectCommunicator(new Communicator(new IFrameMessageHandler(() => parent), Constants.CommunicationChannels.injectedAndUi)); - - // Check the options passed in to determine what kind of Communicator we need to talk to the background task - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.setInjectOptions, (options: ClipperInjectOptions) => { - this.setState({ injectOptions: options }); - - if (this.state.injectOptions.useInlineBackgroundWorker) { - let background = new InlineExtension(); - let backgroundMessageHandler = background.getInlineMessageHandler(); - let uiMessageHandler = new InlineMessageHandler(backgroundMessageHandler); - backgroundMessageHandler.setOtherSide(uiMessageHandler); - Clipper.setExtensionCommunicator(new Communicator(uiMessageHandler, Constants.CommunicationChannels.extensionAndUi)); - } else { - Clipper.setExtensionCommunicator(new Communicator(new IFrameMessageHandler(() => parent), Constants.CommunicationChannels.extensionAndUi)); - } - this.initializeExtensionCommunicator(clientInfo); - Clipper.getExtensionCommunicator().subscribeAcrossCommunicator(Clipper.sessionId, Constants.SmartValueKeys.sessionId); - Clipper.logger = new CommunicatorLoggerPure(Clipper.getExtensionCommunicator()); - - this.initializeInjectCommunicator(pageInfo, clientInfo); - - // When tabbing from outside the iframe, we want to set focus to the lowest tabindex element in our iframe - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.tabToLowestIndexedElement, () => { - let tabbables = document.querySelectorAll("[tabindex]"); - let lowestTabIndexElement: HTMLElement; - if (tabbables.length > 0) { - for (let i = 0; i < tabbables.length; i++) { - let tabbable = tabbables[i] as HTMLElement; - if (!lowestTabIndexElement || tabbable.tabIndex < lowestTabIndexElement.tabIndex) { - if (tabbable.tabIndex >= 0) { - lowestTabIndexElement = tabbable; - } - } - } - - lowestTabIndexElement.focus(); - } - }); - - // initialize here since it depends on storage in the extension - this.initializeNumSuccessfulClips(); - RatingsHelper.preCacheNeededValues(); - }); - - clientInfo.subscribe((updatedClientInfo) => { - if (updatedClientInfo) { - // The default Clip mode now also depends on clientInfo, in addition to pageInfo - // TODO: Don't do this if they already have a mode chosen (once we are updating the pageInfo more object more often) - this.state.setState({ currentMode: this.state.currentMode.set(this.getDefaultClipMode()) }); - - /** - * The following initializations are necessary to make use of - * an offscreen document from the clipper UI. - */ - if (updatedClientInfo.clipperType === ClientType.ChromeExtension || updatedClientInfo.clipperType === ClientType.EdgeExtension) { - WebExtension.browser = chrome; - WebExtension.offscreenUrl = chrome.runtime.getURL("offscreen.html"); - } else { - // Do nothing since clipper has been deprecated for other browsers - } - } - }); - } - - private initializeSmartValues() { - this.state.currentMode.subscribe((newMode: ClipMode) => { - switch (newMode) { - case ClipMode.FullPage: - case ClipMode.Augmentation: - Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.updatePageInfoIfUrlChanged); - break; - default: - break; - } - }, { callOnSubscribe: false }); - } - - private initializeNumSuccessfulClips(): void { - Clipper.getStoredValue(ClipperStorageKeys.numSuccessfulClips, (numClipsAsStr: string) => { - let numClips: number = parseInt(numClipsAsStr, 10); - this.state.numSuccessfulClips = ObjectUtils.isNullOrUndefined(numClips) || isNaN(numClips) ? 0 : numClips; - }); - } - - private getDefaultClipMode(): ClipMode { - if (this.state && this.state.invokeOptions) { - switch (this.state.invokeOptions.invokeMode) { - case InvokeMode.ContextImage: - // We don't want to be stuck in region mode if there are 0 images - if (this.state.regionResult.data.length > 0) { - return ClipMode.Region; - } - break; - case InvokeMode.ContextTextSelection: - return ClipMode.Selection; - default: - break; - } - } - - if (this.state && this.state.pageInfo) { - if (this.state.pageInfo.contentType === OneNoteApi.ContentType.EnhancedUrl) { - return ClipMode.Pdf; - } - - if (UrlUtils.onWhitelistedDomain(this.state.pageInfo.rawUrl)) { - return ClipMode.Augmentation; - } - } - - return ClipMode.FullPage; - } - - private updateFrameHeight(newContainerHeight: number) { - Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.updateFrameHeight, { param: newContainerHeight }); - } - - private handleSignIn(authType: AuthType) { - Clipper.logger.logUserFunnel(Log.Funnel.Label.AuthAttempted); - let handleSignInEvent = new Log.Event.PromiseEvent(Log.Event.Label.HandleSignInEvent); - - this.setState({ userResult: { status: Status.InProgress } }); - - type ErrorObject = { - updateReason: UpdateReason, - correlationId?: string, - error: string, - errorDescription: string - }; - - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.signInUser, { - param: authType, callback: (data: UserInfo | ErrorObject) => { - // For cleaner referencing - // TODO: This kind of thing should go away as we move the communicator to be Promise based. - let updatedUser = data as UserInfo; - let errorObject = data as ErrorObject; - - let errorsFound = errorObject.error || errorObject.errorDescription; - if (errorsFound) { - errorObject.errorDescription = AuthType[authType] + ": " + errorObject.error + ": " + errorObject.errorDescription; - - this.state.setState({ userResult: { status: Status.Failed, data: errorObject } }); - - handleSignInEvent.setStatus(Log.Status.Failed); - handleSignInEvent.setFailureInfo({ error: errorObject.errorDescription }); - handleSignInEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, errorObject.correlationId); - - Clipper.logger.logUserFunnel(Log.Funnel.Label.AuthSignInFailed); - } - - let userInfoReturned = updatedUser && !!updatedUser.user; - if (userInfoReturned) { - Clipper.storeValue(ClipperStorageKeys.hasPatchPermissions, "true"); - Clipper.logger.logUserFunnel(Log.Funnel.Label.AuthSignInCompleted); - } - - handleSignInEvent.setCustomProperty(Log.PropertyName.Custom.UserInformationReturned, userInfoReturned); - handleSignInEvent.setCustomProperty(Log.PropertyName.Custom.SignInCancelled, !errorsFound && !userInfoReturned); - - Clipper.logger.logEvent(handleSignInEvent); - }}); - } - - private handleSignOut(authType: string): void { - Clipper.storeValue(ClipperStorageKeys.isUserLoggedIn, "false", () => { - this.state.setState(this.getSignOutState()); - }); - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.signOutUser, { param: AuthType[authType] }); - - Clipper.logger.logUserFunnel(Log.Funnel.Label.SignOut); - Clipper.logger.logSessionEnd(Log.Session.EndTrigger.SignOut); - Clipper.logger.logSessionStart(); - } - - private getSignOutState(): ClipperState { - let signOutState = this.getResetState(); - signOutState.saveLocation = undefined; - signOutState.userResult = undefined; - return signOutState; - } - - private handleStartClip(): void { - Clipper.logger.logUserFunnel(Log.Funnel.Label.ClipAttempted); - - this.state.setState({ userResult: { status: Status.InProgress, data: this.state.userResult.data } }); - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.ensureFreshUserBeforeClip, { callback: (updatedUser: UserInfo) => { - if (updatedUser && updatedUser.user) { - let currentMode = this.state.currentMode.get(); - if (currentMode === ClipMode.FullPage) { - // A page info refresh needs to be triggered if the url has changed right before the clip action - Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.updatePageInfoIfUrlChanged, { - callback: () => { - this.startClip(); - } - }); - } else if (currentMode === ClipMode.Bookmark) { - // set the rendered bookmark preview HTML as the exact HTML to send to OneNote - let previewBodyHtml = document.getElementById("previewBody").innerHTML; - this.state.setState({ - bookmarkPreviewInfo: { previewBodyHtml: previewBodyHtml } - }); - - this.startClip(); - } else { - this.startClip(); - } - } - }}); - } - - private clearKeepAlive(): void { - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.clearKeepAlive); - } - - private startClip(): void { - this.clearKeepAlive(); - this.state.setState({ oneNoteApiResult: { status: Status.InProgress } }); - - this.storeLastClippedInformation(); - SaveToOneNoteLogger.logClip(this.state); - - let clipEvent = new Log.Event.PromiseEvent(Log.Event.Label.ClipToOneNoteAction); - - (new OneNoteSaveableFactory(this.state)).getSaveable().then((saveable) => { - let saveOptions: SaveToOneNoteOptions = { - page: saveable, - saveLocation: this.state.saveLocation, - progressCallback: this.updateClipSaveProgress.bind(this) - }; - - let saveToOneNote = new SaveToOneNote(this.state.userResult.data.user.accessToken); - saveToOneNote.save(saveOptions).then((responsePackage: OneNoteApi.ResponsePackage) => { - let createPageResponse = Array.isArray(responsePackage) ? responsePackage[0] : responsePackage; - clipEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, createPageResponse.request.getResponseHeader(Constants.HeaderValues.correlationId)); - - let numSuccessfulClips = this.state.numSuccessfulClips + 1; - Clipper.storeValue(ClipperStorageKeys.numSuccessfulClips, numSuccessfulClips.toString()); - this.state.setState({ - oneNoteApiResult: { data: createPageResponse.parsedResponse, status: Status.Succeeded }, - numSuccessfulClips: numSuccessfulClips, - showRatingsPrompt: RatingsHelper.shouldShowRatingsPrompt(this.state) - }); - }).catch((error: OneNoteApi.RequestError) => { - OneNoteApiUtils.logOneNoteApiRequestError(clipEvent, error); - this.state.setState({ oneNoteApiResult: { data: error, status: Status.Failed } }); - }).then(() => { - Clipper.logger.logEvent(clipEvent); - }); - }); - } - - private updateClipSaveProgress(numItemsCompleted: number, numItemsTotal: number): void { - this.state.setState({ - clipSaveStatus: { - numItemsCompleted: numItemsCompleted, - numItemsTotal: numItemsTotal - } - }); - } - - private storeLastClippedInformation() { - Clipper.storeValue(ClipperStorageKeys.lastClippedDate, Date.now().toString()); - - if (this.state.currentMode.get() === ClipMode.Pdf) { - Clipper.storeValue(ClipperStorageKeys.lastClippedTooltipTimeBase + TooltipType[TooltipType.Pdf], Date.now().toString()); - } - - if (this.state.currentMode.get() === ClipMode.Augmentation) { - // Record lastClippedDate for each different augmentationMode so we can upsell the augmentation mode - // to users who haven't Clipped this mode in a while - let augmentationTypeAsString = AugmentationHelper.getAugmentationType(this.state); - Clipper.storeValue(ClipperStorageKeys.lastClippedTooltipTimeBase + augmentationTypeAsString, Date.now().toString()); - } - - VideoUtils.videoDomainIfSupported(this.state.pageInfo.rawUrl).then((isVideoDomainSupported) => { - if (isVideoDomainSupported) { - Clipper.storeValue(ClipperStorageKeys.lastClippedTooltipTimeBase + TooltipType[TooltipType.Video], Date.now().toString()); - } - }); - } - - private static shouldShowOptions(state: ClipperState): boolean { - return (state.uiExpanded && - ClipperStateUtilities.isUserLoggedIn(state) && - state.oneNoteApiResult.status === Status.NotStarted && - !state.badState); - } - - private static shouldShowPreviewViewer(state: ClipperState): boolean { - return (this.shouldShowOptions(state) && - (state.currentMode.get() !== ClipMode.Region || - state.regionResult.status === Status.Succeeded)); - } - - private static shouldShowRegionSelector(state: ClipperState): boolean { - return (this.shouldShowOptions(state) && - state.currentMode.get() === ClipMode.Region && - state.regionResult.status !== Status.Succeeded); - } - - private static shouldShowMainController(state: ClipperState): boolean { - return state.regionResult.status !== Status.InProgress || state.badState; - } - - render() { - let previewViewerItem = ClipperClass.shouldShowPreviewViewer(this.state) ? - : - undefined; - let regionSelectorItem = ClipperClass.shouldShowRegionSelector(this.state) ? : undefined; - let mainControllerStyle = ClipperClass.shouldShowMainController(this.state) ? { } : { display: "none" }; - - return ( -
    - {previewViewerItem} - {regionSelectorItem} -
    - -
    -
    - ); - } -} - -Polyfills.init(); - -// Catch any unhandled exceptions and log them -let oldOnError = self.onerror; -self.onerror = (message: string, filename?: string, lineno?: number, colno?: number, error?: Error) => { - let callStack = error ? Log.Failure.getStackTrace(error) : "[unknown stacktrace]"; - - Clipper.logger.logFailure(Log.Failure.Label.UnhandledExceptionThrown, Log.Failure.Type.Unexpected, - { error: message + " (" + filename + ":" + lineno + ":" + colno + ") at " + callStack }, "ClipperUI"); - - if (oldOnError) { - oldOnError(message, filename, lineno, colno, error); - } -}; - -let component = ClipperClass.componentize(); -m.mount(document.getElementById("clipperUIPlaceholder"), component); -export {component as Clipper} diff --git a/src/scripts/clipperUI/clipperState.ts b/src/scripts/clipperUI/clipperState.ts index 56cfe46d..dd9c2eb0 100644 --- a/src/scripts/clipperUI/clipperState.ts +++ b/src/scripts/clipperUI/clipperState.ts @@ -1,40 +1,28 @@ import {ClientInfo} from "../clientInfo"; import {PageInfo} from "../pageInfo"; -import {PdfPreviewInfo, PreviewGlobalInfo, PreviewInfo} from "../previewInfo"; -import {StringUtils} from "../stringUtils"; import {UserInfo} from "../userInfo"; import {SmartValue} from "../communicator/smartValue"; -import {AugmentationResult} from "../contentCapture/augmentationHelper"; -import {FullPageScreenshotResult} from "../contentCapture/fullPageScreenshotHelper"; -import {PdfScreenshotResult} from "../contentCapture/pdfScreenshotHelper"; -import {BookmarkResult} from "../contentCapture/bookmarkHelper"; - -import {ClipperInjectOptions} from "../extensions/clipperInject"; import {InvokeOptions} from "../extensions/invokeOptions"; import {ClipMode} from "./clipMode"; import {Status} from "./status"; -import {ClipSaveStatus} from "./clipSaveStatus"; +// Minimal ClipperState type retained for clipperUrls.ts feedback URL +// generation. The full V1 ClipperState (with augmentation/full-page/PDF/ +// bookmark result fields, ClipperInjectOptions, etc.) was removed alongside +// the V1 sidebar — the V3 renderer carries its own state in renderer.ts. export interface DataResult { data?: T; status: Status; } -export interface ClipperStateProp { - clipperState: ClipperState; -} - export interface ClipperState { - injectOptions?: ClipperInjectOptions; uiExpanded?: boolean; - fetchLocStringStatus?: Status; - // Initialized at the start of the Clipper's instantiation to determine initial mode. Additionally, - // is re-fetched whenever the Clipper visbility is toggled on + // Initialized at the start of the Clipper's instantiation invokeOptions?: InvokeOptions; // External "static" data @@ -43,37 +31,12 @@ export interface ClipperState { clientInfo?: ClientInfo; // User input - currentMode?: SmartValue; // Full, Region, Augmentation - saveLocation?: string; // Result from the SectionPicker - - // Content preview data + retrieval status - fullPageResult?: DataResult; - pdfResult?: DataResult>; - regionResult?: DataResult; - augmentationResult?: DataResult; - bookmarkResult?: DataResult; - - // Editable preview content - previewGlobalInfo?: PreviewGlobalInfo; - augmentationPreviewInfo?: PreviewInfo; - bookmarkPreviewInfo?: PreviewInfo; - pdfPreviewInfo?: PdfPreviewInfo; - selectionPreviewInfo?: PreviewInfo; - - // Save to OneNote status - oneNoteApiResult?: DataResult; - clipSaveStatus?: ClipSaveStatus; + currentMode?: SmartValue; + saveLocation?: string; // Should be set when the Web Clipper enters a state that can not be recovered this session badState?: boolean; - // Used for determining if user should see ratings prompt - numSuccessfulClips?: number; - showRatingsPrompt?: boolean; - - // Element ID to focus after next render - focusOnRender?: string; - setState?: (partialState: ClipperState) => void; reset?: () => void; } diff --git a/src/scripts/clipperUI/clipperStateUtilities.ts b/src/scripts/clipperUI/clipperStateUtilities.ts deleted file mode 100644 index ee6b5b91..00000000 --- a/src/scripts/clipperUI/clipperStateUtilities.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {ObjectUtils} from "../objectUtils"; - -import * as Log from "../logging/log"; -import {ClipMode} from "./clipMode"; -import {ClipperState} from "./clipperState"; -import {Status} from "./status"; -import { Clipper } from "./frontEndGlobals"; -import { SmartValue } from "../communicator/smartValue"; -import { Constants } from "../constants"; -import { TimeStampedData, CachedHttp } from "../http/cachedHttp"; -import { UserInfo, UpdateReason } from "../userInfo"; -import { sendToOffscreenDocument } from "../communicator/offscreenCommunicator"; -import { OffscreenMessageTypes } from "../communicator/offscreenMessageTypes"; -import { ClipperStorageKeys } from "../storage/clipperStorageKeys"; - -export module ClipperStateUtilities { - export function isUserLoggedIn(state: ClipperState, refreshUserInfo?: boolean): boolean { - if (state.userResult && state.userResult.status && state.userResult.data && !!state.userResult.data.user) { - return true; - } else if (!!refreshUserInfo) { - /** - * Refresh the user info if the user is logged in and the user info is not available. - * This could have possibly happened due to inactivity of the service worker. - */ - sendToOffscreenDocument(OffscreenMessageTypes.getFromLocalStorage, { - key: ClipperStorageKeys.isUserLoggedIn - }).then((isUserLoggedIn) => { - if (isUserLoggedIn === "true") { - let userInfoUpdateCb = (updatedUser: UserInfo) => { - if (updatedUser) { - let userInfoUpdatedEvent = new Log.Event.BaseEvent(Log.Event.Label.UserInfoUpdated); - userInfoUpdatedEvent.setCustomProperty(Log.PropertyName.Custom.UserUpdateReason, UpdateReason[updatedUser.updateReason]); - userInfoUpdatedEvent.setCustomProperty(Log.PropertyName.Custom.LastUpdated, new Date(updatedUser.lastUpdated).toUTCString()); - Clipper.logger.logEvent(userInfoUpdatedEvent); - } - - if (updatedUser && updatedUser.user) { - let timeStampedData: TimeStampedData = { - data: updatedUser.user, - lastUpdated: updatedUser.lastUpdated - }; - - // The user SV should never be set with expired user information - let tokenHasExpiredForLoggedInUser = CachedHttp.valueHasExpired(timeStampedData, (updatedUser.user.accessTokenExpiration * 1000) - 180000); - if (tokenHasExpiredForLoggedInUser) { - Clipper.logger.logFailure(Log.Failure.Label.UserSetWithInvalidExpiredData, Log.Failure.Type.Unexpected); - } - - state.setState({ userResult: { status: Status.Succeeded, data: updatedUser } }); - Clipper.logger.setContextProperty(Log.Context.Custom.AuthType, updatedUser.user.authType); - Clipper.logger.setContextProperty(Log.Context.Custom.UserInfoId, updatedUser.user.cid); - } else { - state.setState({ userResult: { status: Status.Failed, data: updatedUser } }); - } - }; - - state.setState({ userResult: { status: Status.InProgress } }); - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.getInitialUser, { - callback: (freshInitialUser: UserInfo) => { - if (freshInitialUser && freshInitialUser.user) { - Clipper.logger.logUserFunnel(Log.Funnel.Label.AuthAlreadySignedIn); - } else if (!freshInitialUser) { - userInfoUpdateCb(freshInitialUser); - } - Clipper.getExtensionCommunicator().subscribeAcrossCommunicator(new SmartValue(), Constants.SmartValueKeys.user, (updatedUser: UserInfo) => { - userInfoUpdateCb(updatedUser); - }); - } - }); - } - }); - /** - * There isn't a need to await the response from the offscreen document since the isUserLoggedIn - * function will eventually return true once the user info is updated, and the signed in panel - * will be shown to the user. - */ - return false; - } - } - - export function isMsaUser(state: ClipperState): boolean { - return state.userResult && state.userResult.data && state.userResult.data.user && state.userResult.data.user.authType && (state.userResult.data.user.authType.toLowerCase() === "msa"); - } - - export function clipButtonEnabled(clipperState: ClipperState): boolean { - let currentMode = clipperState.currentMode.get(); - switch (currentMode) { - case ClipMode.Pdf: - if (!clipperState.pdfPreviewInfo.isLocalFileAndNotAllowed) { - return false; - } else if (clipperState.pdfResult.status !== Status.Succeeded) { - return false; - } else if (clipperState.pdfPreviewInfo.allPages) { - return true; - } else if (!clipperState.pdfPreviewInfo.allPages && ObjectUtils.isNullOrUndefined(clipperState.pdfPreviewInfo.selectedPageRange)) { - return false; - } - - // If the user has an invalidPageRange, the clipButton is still enabled, - // but when the user clips, we short circuit it and display a message instead - return true; - case ClipMode.FullPage: - // In the past, we used to allow clips while this is pending, however, we found some pages can't be clipped in full page mode - let fullPageScreenshotResult = clipperState.fullPageResult; - return fullPageScreenshotResult.status === Status.Succeeded; - case ClipMode.Region: - let regionResult = clipperState.regionResult; - return regionResult.status === Status.Succeeded && regionResult.data && regionResult.data.length > 0; - case ClipMode.Augmentation: - let augmentationResult = clipperState.augmentationResult; - return augmentationResult.status === Status.Succeeded && augmentationResult.data && !!augmentationResult.data.ContentInHtml; - case ClipMode.Bookmark: - let bookmarkResult = clipperState.bookmarkResult; - return bookmarkResult.status === Status.Succeeded; - case ClipMode.Selection: - // The availability of this mode is passed together with the selected text, so it's always available - return true; - default: - return undefined; - } - } -} diff --git a/src/scripts/clipperUI/componentBase.ts b/src/scripts/clipperUI/componentBase.ts deleted file mode 100644 index f2875c47..00000000 --- a/src/scripts/clipperUI/componentBase.ts +++ /dev/null @@ -1,213 +0,0 @@ -import {Constants} from "../constants"; -import {Clipper} from "./frontEndGlobals"; - -export interface EnableInvokeParams { - callback?: Function; - tabIndex: number; - args?: any; - idOverride?: string; -} - -export interface EnableAriaParams extends EnableInvokeParams { - ariaSetName: string; - autoSelect?: boolean; -} - -export abstract class ComponentBase { - public state: TState; - public props: TProps; - public refs: any; - - constructor(props: TProps) { - this.props = props; - this.state = this.getInitialState(); - this.refs = {}; - } - - public abstract render(props?: TProps); - - public getInitialState(): TState { - return {} as TState; - } - - public setState(newPartialState: TState) { - m.startComputation(); - for (let key in newPartialState) { - if (newPartialState.hasOwnProperty(key)) { - this.state[key] = newPartialState[key]; - } - } - m.endComputation(); - } - - public ref(name: string) { - return { - config: (element: HTMLElement) => { - this.refs[name] = element; - } - }; - } - - public onElementDraw(handleMethod: (element: HTMLElement, isFirstDraw: boolean) => void) { - // Because of the way mithril does the callbacks, we need to rescope it so that "this" points to the class - handleMethod = handleMethod.bind(this); - return { - config: (element: HTMLElement, isInitialized: boolean) => { - handleMethod(element, !isInitialized); - } - }; - } - - public onElementFirstDraw(handleMethod: (element: HTMLElement) => void) { - // Because of the way mithril does the callbacks, we need to rescope it so that "this" points to the class - handleMethod = handleMethod.bind(this); - return { - config: (element: HTMLElement, isInitialized: boolean) => { - if (!isInitialized) { - handleMethod(element); - } - } - }; - } - - /* - * Helper which handles tabIndex, clicks, and keyboard navigation for a component that is part of an Aria Set - * - * Also hides the outline if they are using a mouse, but shows it if they are using the keyboard - * (idea from http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/) - */ - enableAriaInvoke({callback, tabIndex, args, idOverride, ariaSetName, autoSelect = false}: EnableAriaParams) { - if (callback) { - callback = callback.bind(this, args); - } - - let invokeAttributes = this.enableInvoke({callback: callback, tabIndex: tabIndex, args: args, idOverride: idOverride}); - let oldKeyUp = invokeAttributes.onkeyup; - - invokeAttributes.onkeyup = (e: KeyboardEvent) => { - let currentTargetElement = e.currentTarget as HTMLElement; - - oldKeyUp(e); - - if (e.which === Constants.KeyCodes.home) { - let firstInSet = 1; - ComponentBase.focusOnButton(ariaSetName, firstInSet, autoSelect); - } else if (e.which === Constants.KeyCodes.end) { - let lastInSet = parseInt(currentTargetElement.getAttribute("aria-setsize"), 10); - ComponentBase.focusOnButton(ariaSetName, lastInSet, autoSelect); - } - - let posInSet = parseInt(currentTargetElement.getAttribute("aria-posinset"), 10); - - if (e.which === Constants.KeyCodes.up || e.which === Constants.KeyCodes.left) { - if (posInSet <= 1) { - return; - } - let nextPosInSet = posInSet - 1; - ComponentBase.focusOnButton(ariaSetName, nextPosInSet, autoSelect); - } else if (e.which === Constants.KeyCodes.down || e.which === Constants.KeyCodes.right) { - let setSize = parseInt(currentTargetElement.getAttribute("aria-setsize"), 10); - if (posInSet >= setSize) { - return; - } - let nextPosInSet = posInSet + 1; - ComponentBase.focusOnButton(ariaSetName, nextPosInSet, autoSelect); - } - }; - - invokeAttributes["data-" + Constants.CustomHtmlAttributes.setNameForArrowKeyNav] = ariaSetName; - - return invokeAttributes; - } - - /* - * Helper which handles tabIndex, clicks, and keyboard navigation. - * - * Also hides the outline if they are using a mouse, but shows it if they are using the keyboard - * (idea from http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/) - * - * Example use: - * Click Me - */ - public enableInvoke({callback, tabIndex, args, idOverride}: EnableInvokeParams) { - // Because of the way mithril does the callbacks, we need to rescope it so that "this" points to the class - if (callback) { - callback = callback.bind(this, args); - } - - return { - onclick: (e: MouseEvent) => { - let element = e.currentTarget as HTMLElement; - ComponentBase.triggerSelection(element, idOverride, callback, e); - }, - onkeyup: (e: KeyboardEvent) => { - let element = e.currentTarget as HTMLElement; - if (e.which === Constants.KeyCodes.enter || e.which === Constants.KeyCodes.space) { - // Hitting Enter on tags that contains an href automatically fire the click event, so don't do it again - if (!(element.tagName === "A" && element.hasAttribute("href"))) { - ComponentBase.triggerSelection(element, undefined, callback, e); - } - } else if (e.which === Constants.KeyCodes.tab) { - // Since they are using the keyboard, revert to the default value of the outline so it is visible - element.style.outlineStyle = ""; - } - }, - onkeydown: (e: KeyboardEvent) => { - if (e.which === Constants.KeyCodes.space || e.which === Constants.KeyCodes.up - || e.which === Constants.KeyCodes.down || e.which === Constants.KeyCodes.left - || e.which === Constants.KeyCodes.right || e.which === Constants.KeyCodes.home - || e.which === Constants.KeyCodes.end) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - }, - onmousedown: (e: MouseEvent) => { - let element = e.currentTarget as HTMLElement; - element.style.outlineStyle = "none"; - }, - tabIndex: tabIndex, - }; - } - - private static triggerSelection(element: HTMLElement, idOverride: string, callback: Function, e: Event) { - // Intentionally sending click event before handling the method - // TODO replace this comment with a test that validates the call order is correct - let id = idOverride ? idOverride : element.id; - - Clipper.logger.logClickEvent(id); - - if (callback) { - callback(e); - } - } - - private static focusOnButton(setNameForArrowKeyNav: string, posInSet: number, autoSelect: boolean) { - const buttons = document.querySelectorAll("[data-" + Constants.CustomHtmlAttributes.setNameForArrowKeyNav + "=" + setNameForArrowKeyNav + "]"); - for (let i = 0; i < buttons.length; i++) { - let selectable = buttons[i] as HTMLElement; - let ariaIntForEach = parseInt(selectable.getAttribute("aria-posinset"), 10); - if (ariaIntForEach === posInSet) { - selectable.style.outlineStyle = ""; - autoSelect ? selectable.click() : selectable.focus(); - return; - } - } - } - - // Note: currently all components NEED either a child or attribute to work with the MSX transformer. - // This won't work, but this will work. - public static componentize() { - let returnValue: any = () => { - }; - returnValue.controller = (props: any) => { - // Instantiate an instance of the inheriting class - return new (this)(props); - }; - returnValue.view = (controller: any, props: any) => { - controller.props = props; - return controller.render(); - }; - - return returnValue; - } -} diff --git a/src/scripts/clipperUI/components/annotationInput.tsx b/src/scripts/clipperUI/components/annotationInput.tsx deleted file mode 100644 index f573d611..00000000 --- a/src/scripts/clipperUI/components/annotationInput.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as _ from "lodash"; -import {Constants} from "../../constants"; -import {ExtensionUtils} from "../../extensions/extensionUtils"; -import {Localization} from "../../localization/localization"; -import {PreviewGlobalInfo} from "../../previewInfo"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; - -class AnnotationInputClass extends ComponentBase<{}, ClipperStateProp> { - private static textAreaListenerAttached = false; - - // TODO: change this to a config passed into the textarea? - private addTextAreaListener() { - document.addEventListener("input", (event) => { - let element = event.target; - let annotationField = document.getElementById(Constants.Ids.annotationField) as HTMLTextAreaElement; - if (!!element && element === annotationField) { - this.handleAnnotationFieldChanged(annotationField.value); - } - }); - } - - private handleAnnotationFieldChanged(annotationValue: string) { - this.props.clipperState.setState({ - previewGlobalInfo: { - previewTitleText: this.props.clipperState.previewGlobalInfo.previewTitleText, - annotation: annotationValue, - fontSize: this.props.clipperState.previewGlobalInfo.fontSize, - serif: this.props.clipperState.previewGlobalInfo.serif - } - }); - } - - render() { - if (!AnnotationInputClass.textAreaListenerAttached) { - this.addTextAreaListener(); - AnnotationInputClass.textAreaListenerAttached = true; - } - - return ( -
    -
    -					
    -						{!!this.props.clipperState.previewGlobalInfo.annotation ? this.props.clipperState.previewGlobalInfo.annotation : ""}
    -					
    -					
    -
    - -
    - ); - } -} - -let component = AnnotationInputClass.componentize(); -export { component as AnnotationInput }; diff --git a/src/scripts/clipperUI/components/ariaSetProps.ts b/src/scripts/clipperUI/components/ariaSetProps.ts deleted file mode 100644 index 6bc07466..00000000 --- a/src/scripts/clipperUI/components/ariaSetProps.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AriaSetProps { - "aria-posinset": number; - "aria-setsize": number; -} diff --git a/src/scripts/clipperUI/components/closeButton.tsx b/src/scripts/clipperUI/components/closeButton.tsx deleted file mode 100644 index e221f7bb..00000000 --- a/src/scripts/clipperUI/components/closeButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import {Constants} from "../../constants"; - -import {ExtensionUtils} from "../../extensions/extensionUtils"; -import {Localization} from "../../localization/localization"; - -import {ComponentBase} from "../componentBase"; - -export interface CloseButtonProps { - onClickHandler: () => void; - onClickHandlerParams: any[]; -} - -class CloseButtonClass extends ComponentBase<{}, CloseButtonProps> { - getInitialState(): {} { - return {}; - } - - render() { - return ( -
    - - - -
    - ); - } -} - -let component = CloseButtonClass.componentize(); -export {component as CloseButton}; diff --git a/src/scripts/clipperUI/components/footer.tsx b/src/scripts/clipperUI/components/footer.tsx deleted file mode 100644 index e2e20133..00000000 --- a/src/scripts/clipperUI/components/footer.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import {BrowserUtils} from "../../browserUtils"; -import {ClientType} from "../../clientType"; -import {ClipperUrls} from "../../clipperUrls"; -import {Constants} from "../../constants"; - -import {ExtensionUtils} from "../../extensions/extensionUtils"; - -import {Localization} from "../../localization/localization"; - -import * as Log from "../../logging/log"; - -import {Clipper} from "../frontEndGlobals"; -import {ClipperStateProp} from "../clipperState"; -import {ClipperStateUtilities} from "../clipperStateUtilities"; -import {ComponentBase} from "../componentBase"; - -interface FooterState { - userSettingsOpened: boolean; -} - -interface FooterProps extends ClipperStateProp { - onSignOutInvoked: (authType: string) => void; -} - -class FooterClass extends ComponentBase { - private feedbackUrl: string; - private feedbackWindowRef: Window; - - getFeedbackWindowRef(): Window { - return this.feedbackWindowRef; - } - - getInitialState() { - return { userSettingsOpened: false }; - } - - userControlHandler() { - this.setState({ userSettingsOpened: !this.state.userSettingsOpened }); - } - - handleSignOutButton() { - this.setState({ userSettingsOpened: false }); - if (this.props.onSignOutInvoked) { - this.props.onSignOutInvoked(this.props.clipperState.userResult.data.user.authType); - } - } - - handleFeedbackButton(args: any, event: MouseEvent) { - // In order to make it easy to collect some information from the user, we're hijacking - // the feedback button to show the relevant info. - if (event.altKey && event.shiftKey) { - let debugMessage = ClientType[this.props.clipperState.clientInfo.clipperType] + ": " + this.props.clipperState.clientInfo.clipperVersion; - debugMessage += "\nID: " + this.props.clipperState.clientInfo.clipperId; - debugMessage += "\nUsid: " + Clipper.getUserSessionId(); - - Clipper.logger.logEvent(new Log.Event.BaseEvent(Log.Event.Label.DebugFeedback)); - - self.alert(debugMessage); - return; - } - - if (!this.feedbackWindowRef || this.feedbackWindowRef.closed) { - if (!this.feedbackUrl) { - this.feedbackUrl = ClipperUrls.generateFeedbackUrl(this.props.clipperState, Clipper.getUserSessionId(), Constants.LogCategories.oneNoteClipperUsage); - } - this.feedbackWindowRef = BrowserUtils.openPopupWindow(this.feedbackUrl); - } - } - - render() { - let showUserInfo = ClipperStateUtilities.isUserLoggedIn(this.props.clipperState); - let isMsaUser = ClipperStateUtilities.isMsaUser(this.props.clipperState); - - return ( -
    -
    -
    - {!isMsaUser - ? ( - - {Localization.getLocalizedString("WebClipper.Action.Feedback") } - ) - : undefined - } -
    - {showUserInfo - ? (
    - -
    - { - this.props.clipperState.userResult.data.user.fullName - ?
    {this.props.clipperState.userResult.data.user.fullName}
    - :
    {Localization.getLocalizedString("WebClipper.Label.SignedIn")}
    - } - { - this.props.clipperState.userResult.data.user.emailAddress - ?
    {this.props.clipperState.userResult.data.user.emailAddress}
    - : "" - } -
    - -
    -
    ) - : undefined - } -
    - {this.state.userSettingsOpened - ? (
    -
    -
    - - - {Localization.getLocalizedString("WebClipper.Action.SignOut")} - -
    -
    ) - : undefined - } -
    - ); - } -} - -let component = FooterClass.componentize(); -export {component as Footer}; diff --git a/src/scripts/clipperUI/components/modeButton.tsx b/src/scripts/clipperUI/components/modeButton.tsx deleted file mode 100644 index 6fafc2ab..00000000 --- a/src/scripts/clipperUI/components/modeButton.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import {Constants} from "../../constants"; -import {Localization} from "../../localization/localization"; -import {ClipMode} from "../clipMode"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; -import {AriaSetProps} from "./ariaSetProps"; - -export interface PropsForModeElementNoAriaGrouping { - imgSrc: string; - label: string; - myMode: ClipMode; - selected?: boolean; - tabIndex?: number; - onModeSelected: (modeButton: ClipMode) => void; - tooltipText?: string; -} - -export interface PropsForModeButton extends PropsForModeElementNoAriaGrouping, AriaSetProps, ClipperStateProp { } - -class ModeButtonClass extends ComponentBase<{}, PropsForModeButton> { - initiallySetFocus(element: HTMLElement) { - if (this.props.selected) { - element.focus(); - } - } - - handleFocusOnRender(element: HTMLElement) { - // Check if this button should receive focus after render - if (this.props.clipperState.focusOnRender === element.id) { - element.focus(); - // Clear the focusOnRender flag - this.props.clipperState.setState({ - focusOnRender: undefined - }); - } - } - - buttonHandler() { - this.props.onModeSelected(this.props.myMode); - } - - public render() { - let className = "modeButton"; - if (this.props.selected) { - className += " selected"; - } - let clipMode: string = ClipMode[this.props.myMode]; - clipMode = clipMode[0].toLowerCase() + clipMode.slice(1); - let idName: string = clipMode + "Button"; - let ariaLabel = this.props.tooltipText ? `${this.props.label} clipping mode, ${this.props.tooltipText}` : `${this.props.label} clipping mode`; - - return ( - - - - {this.props.label} - - - ); - } -} - -let component = ModeButtonClass.componentize(); -export {component as ModeButton}; diff --git a/src/scripts/clipperUI/components/modeButtonSelector.tsx b/src/scripts/clipperUI/components/modeButtonSelector.tsx deleted file mode 100644 index b05d996b..00000000 --- a/src/scripts/clipperUI/components/modeButtonSelector.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import {Constants} from "../../constants"; -import {AugmentationHelper} from "../../contentCapture/augmentationHelper"; -import {ExtensionUtils} from "../../extensions/extensionUtils"; -import {InvokeMode} from "../../extensions/invokeOptions"; -import {Localization} from "../../localization/localization"; -import {ClipMode} from "../clipMode"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; -import {ModeButton, PropsForModeElementNoAriaGrouping} from "./modeButton"; - -class ModeButtonSelectorClass extends ComponentBase<{}, ClipperStateProp> { - onModeSelected(newMode: ClipMode) { - this.props.clipperState.setState({ - currentMode: this.props.clipperState.currentMode.set(newMode), - focusOnRender: Constants.Ids.previewInnerContainer - }); - }; - - private getPdfButtonProps(currentMode: ClipMode, tabIndex: number): PropsForModeElementNoAriaGrouping { - if (this.props.clipperState.pageInfo.contentType !== OneNoteApi.ContentType.EnhancedUrl) { - return undefined; - } - - return { - imgSrc: ExtensionUtils.getImageResourceUrl("pdf.png"), - label: Localization.getLocalizedString("WebClipper.ClipType.Pdf.Button"), - myMode: ClipMode.Pdf, - selected: currentMode === ClipMode.Pdf, - onModeSelected: this.onModeSelected.bind(this), - tooltipText: Localization.getLocalizedString("WebClipper.ClipType.Pdf.Button.Tooltip"), - tabIndex: tabIndex - }; - } - - private getAugmentationButtonProps(currentMode: ClipMode, tabIndex: number): PropsForModeElementNoAriaGrouping { - if (this.props.clipperState.pageInfo.contentType === OneNoteApi.ContentType.EnhancedUrl) { - return undefined; - } - - let augmentationType: string = AugmentationHelper.getAugmentationType(this.props.clipperState); - let augmentationLabel: string = Localization.getLocalizedString("WebClipper.ClipType." + augmentationType + ".Button"); - let augmentationTooltip = Localization.getLocalizedString("WebClipper.ClipType.Button.Tooltip").replace("{0}", augmentationLabel); - let buttonSelected: boolean = currentMode === ClipMode.Augmentation; - return { - imgSrc: (augmentationType === "Article") ? ExtensionUtils.getImageResourceUrl("article.svg") : ExtensionUtils.getImageResourceUrl(augmentationType + ".png"), - label: augmentationLabel, - myMode: ClipMode.Augmentation, - selected: buttonSelected, - onModeSelected: this.onModeSelected.bind(this), - tooltipText: augmentationTooltip, - tabIndex: tabIndex - }; - } - - private getFullPageButtonProps(currentMode: ClipMode, tabIndex: number): PropsForModeElementNoAriaGrouping { - if (this.props.clipperState.pageInfo.contentType === OneNoteApi.ContentType.EnhancedUrl) { - return undefined; - } - - return { - imgSrc: ExtensionUtils.getImageResourceUrl("fullpage.svg"), - label: Localization.getLocalizedString("WebClipper.ClipType.ScreenShot.Button"), - myMode: ClipMode.FullPage, - selected: currentMode === ClipMode.FullPage, - onModeSelected: this.onModeSelected.bind(this), - tooltipText: Localization.getLocalizedString("WebClipper.ClipType.ScreenShot.Button.Tooltip"), - tabIndex: tabIndex - }; - } - - private getRegionButtonProps(currentMode: ClipMode, tabIndex: number): PropsForModeElementNoAriaGrouping { - let enableRegionClipping = this.props.clipperState.injectOptions && this.props.clipperState.injectOptions.enableRegionClipping; - let contextImageModeUsed = this.props.clipperState.invokeOptions && this.props.clipperState.invokeOptions.invokeMode === InvokeMode.ContextImage; - - if (!enableRegionClipping && !contextImageModeUsed) { - return undefined; - } - - return { - imgSrc: ExtensionUtils.getImageResourceUrl("region.svg"), - label: Localization.getLocalizedString(this.getRegionButtonLabel()), - myMode: ClipMode.Region, - selected: currentMode === ClipMode.Region, - onModeSelected: this.onModeSelected.bind(this), - tooltipText: Localization.getLocalizedString("WebClipper.ClipType.MultipleRegions.Button.Tooltip"), - tabIndex: tabIndex - }; - } - - private getRegionButtonLabel(): string { - return "WebClipper.ClipType.Region.Button"; - } - - private getSelectionButtonProps(currentMode: ClipMode, tabIndex: number): PropsForModeElementNoAriaGrouping { - if (this.props.clipperState.invokeOptions.invokeMode !== InvokeMode.ContextTextSelection) { - return undefined; - } - - return { - imgSrc: ExtensionUtils.getImageResourceUrl("select.png"), - label: Localization.getLocalizedString("WebClipper.ClipType.Selection.Button"), - myMode: ClipMode.Selection, - selected: currentMode === ClipMode.Selection, - onModeSelected: this.onModeSelected.bind(this), - tooltipText: Localization.getLocalizedString("WebClipper.ClipType.Selection.Button.Tooltip"), - tabIndex: tabIndex - }; - } - - private getBookmarkButtonProps(currentMode: ClipMode, tabIndex: number): PropsForModeElementNoAriaGrouping { - if (this.props.clipperState.pageInfo.rawUrl.indexOf("file:///") === 0) { - return undefined; - } - - return { - imgSrc: ExtensionUtils.getImageResourceUrl("bookmark.svg"), - label: Localization.getLocalizedString("WebClipper.ClipType.Bookmark.Button"), - myMode: ClipMode.Bookmark, - selected: currentMode === ClipMode.Bookmark, - onModeSelected: this.onModeSelected.bind(this), - tooltipText: Localization.getLocalizedString("WebClipper.ClipType.Bookmark.Button.Tooltip"), - tabIndex: tabIndex - }; - } - - private getListOfButtons(): HTMLElement[] { - let currentMode = this.props.clipperState.currentMode.get(); - - // Base tabIndex for mode buttons - they should come before PDF options (60+) and location dropdown (70) - let baseTabIndex = 40; - - let buttonProps = [ - this.getFullPageButtonProps(currentMode, baseTabIndex), - this.getRegionButtonProps(currentMode, baseTabIndex + 1), - this.getAugmentationButtonProps(currentMode, baseTabIndex + 2), - this.getSelectionButtonProps(currentMode, baseTabIndex + 3), - this.getBookmarkButtonProps(currentMode, baseTabIndex + 4), - this.getPdfButtonProps(currentMode, baseTabIndex + 5), - ]; - - let visibleButtons = []; - - let propsForVisibleButtons = buttonProps.filter(attributes => !!attributes); - for (let i = 0; i < propsForVisibleButtons.length; i++) { - let attributes = propsForVisibleButtons[i]; - let ariaPos = i + 1; - visibleButtons.push(); - } - return visibleButtons; - } - - public render() { - let currentMode = this.props.clipperState.currentMode.get(); - - return ( -
    -
    - { this.getListOfButtons() } -
    -
    - ); - } -} - -let component = ModeButtonSelectorClass.componentize(); -export {component as ModeButtonSelector}; diff --git a/src/scripts/clipperUI/components/pdfClipOptions.tsx b/src/scripts/clipperUI/components/pdfClipOptions.tsx deleted file mode 100644 index b6a3d862..00000000 --- a/src/scripts/clipperUI/components/pdfClipOptions.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import {Localization} from "../../localization/localization"; -import {Constants} from "../../constants"; -import {PdfPreviewInfo} from "../../previewInfo"; -import {ExtensionUtils} from "../../extensions/extensionUtils"; -import {ComponentBase} from "../componentBase"; -import {ClipperStateProp} from "../clipperState"; -import {Status} from "../status"; -import {AnimationHelper} from "../animations/animationHelper"; -import {AnimationState} from "../animations/animationState"; -import {AnimationStrategy} from "../animations/animationStrategy"; -import {FadeInAnimationStrategy} from "../animations/fadeInAnimationStrategy"; -import {PdfPageSelectionRadioButton} from "./pdfPageSelectionRadioButton"; -import * as _ from "lodash"; - -interface PdfClipOptionsState { - moreOptionsOpened?: boolean; -} - -class PdfClipOptionsClass extends ComponentBase { - - private hiddenOptionsAnimationStrategy: AnimationStrategy; - - constructor(props: ClipperStateProp) { - super(props); - - this.hiddenOptionsAnimationStrategy = new FadeInAnimationStrategy({ - extShouldAnimateIn: () => { return this.state.moreOptionsOpened; }, - extShouldAnimateOut: () => { return !this.state.moreOptionsOpened; } - }); - } - - getInitialState(): PdfClipOptionsState { - return { - moreOptionsOpened: false - }; - } - - onCheckboxChange(checked: boolean): void { - const pdfHasSucceeded = this.props.clipperState.pdfResult.status === Status.Succeeded; - const pdfIsTooLarge = pdfHasSucceeded && this.props.clipperState.pdfResult.data.get().byteLength > Constants.Settings.maximumMimeSizeLimit; - - if (!pdfHasSucceeded || pdfIsTooLarge) { - return; - } - - _.assign(_.extend(this.props.clipperState.pdfPreviewInfo, { - shouldAttachPdf: checked - } as PdfPreviewInfo), this.props.clipperState.setState); - } - - onDistributionChange(checked: boolean): void { - _.assign(_.extend(this.props.clipperState.pdfPreviewInfo, { - shouldDistributePages: checked - } as PdfPreviewInfo), this.props.clipperState.setState); - } - - onMoreClicked(): void { - this.setState({ - moreOptionsOpened: !this.state.moreOptionsOpened - }); - } - - getDistributePagesCheckbox(): any { - let pdfPreviewInfo = this.props.clipperState.pdfPreviewInfo; - return ( -
    -
    - {pdfPreviewInfo.shouldDistributePages ?
    : ""} -
    - {Localization.getLocalizedString("WebClipper.Label.PdfDistributePagesCheckbox")} -
    -
    - ); - } - - getAttachmentCheckbox(): any { - const pdfHasSucceeded = this.props.clipperState.pdfResult.status === Status.Succeeded; - const pdfIsTooLarge = pdfHasSucceeded && this.props.clipperState.pdfResult.data.get().byteLength > Constants.Settings.maximumMimeSizeLimit; - const disableCheckbox = pdfIsTooLarge || !pdfHasSucceeded; - - if (pdfIsTooLarge) { - return this.getAttachmentIsTooLargeCheckbox(); - } - - return this.getAttachmentPdfCheckbox(pdfHasSucceeded); - } - - getAttachmentIsTooLargeCheckbox(): any { - const disabledClassName = " disabled"; - - return ( -
    - -
    - {Localization.getLocalizedString("WebClipper.Label.PdfTooLargeToAttach")} -
    -
    - ); - } - - getAttachmentPdfCheckbox(enabled: boolean) { - const pdfPreviewInfo = this.props.clipperState.pdfPreviewInfo; - const disabledClassName = enabled ? "" : " disabled"; - - return ( -
    -
    - {pdfPreviewInfo.shouldAttachPdf ?
    : ""} -
    - {Localization.getLocalizedString("WebClipper.Label.AttachPdfFile") + " "} - {Localization.getLocalizedString("WebClipper.Label.AttachPdfFileSubText")} - -
    -
    - ); - } - - private onHiddenOptionsDraw(hiddenOptionsAnimator: HTMLElement) { - this.hiddenOptionsAnimationStrategy.animate(hiddenOptionsAnimator); - - // If the user is rapidly clicking the More button, we want to cancel the current animation to kick off the next one - let currentAnimationState = this.hiddenOptionsAnimationStrategy.getAnimationState(); - if (currentAnimationState === AnimationState.GoingOut && this.state.moreOptionsOpened) { - AnimationHelper.stopAnimationsThen(hiddenOptionsAnimator, () => { - this.hiddenOptionsAnimationStrategy.setAnimationState(AnimationState.Out); - this.setState({ }); - }); - } - } - - render() { - let expandOptionLabel = this.state.moreOptionsOpened ? Localization.getLocalizedString("WebClipper.Action.Less") : Localization.getLocalizedString("WebClipper.Action.More"); - return ( -
    -
    - {Localization.getLocalizedString("WebClipper.Label.PdfOptions")} - - {expandOptionLabel} - -
    - -
    - {this.state.moreOptionsOpened ? -
    - {this.getDistributePagesCheckbox()} - {this.getAttachmentCheckbox()} -
    : undefined} -
    -
    - ); - } -} - -let component = PdfClipOptionsClass.componentize(); -export { component as PdfClipOptions }; diff --git a/src/scripts/clipperUI/components/pdfPageSelectionRadioButton.tsx b/src/scripts/clipperUI/components/pdfPageSelectionRadioButton.tsx deleted file mode 100644 index a9cd19f3..00000000 --- a/src/scripts/clipperUI/components/pdfPageSelectionRadioButton.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import {ClipperStateProp} from "../clipperState"; -import {Constants} from "../../constants"; -import {Localization} from "../../localization/localization"; -import {PdfPreviewInfo} from "../../previewInfo"; -import {Popover} from "./popover"; -import * as _ from "lodash"; -import {StringUtils} from "../../stringUtils"; -import {OperationResult} from "../../operationResult"; -import {ComponentBase} from "../componentBase"; - -export interface RadioButtonGroup { - role?: string; - isAriaSet?: boolean; - innerElements: any[]; -} - -class PdfPageSelectionRadioButton extends ComponentBase<{}, ClipperStateProp> { - private static textAreaListenerAttached = false; - - constructor(props: ClipperStateProp) { - super(props); - if (!PdfPageSelectionRadioButton.textAreaListenerAttached) { - this.addTextAreaListener(); - PdfPageSelectionRadioButton.textAreaListenerAttached = true; - } - } - - getRadioButtonGroups(): RadioButtonGroup[] { - return [this.getRadioButtons()]; - } - - private addTextAreaListener() { - document.addEventListener("input", (event) => { - let element = event.target; - let pageRangeField = document.getElementById(Constants.Ids.rangeInput) as HTMLTextAreaElement; - if (!!element && element === pageRangeField) { - this.onTextChange(pageRangeField.value); - } - }); - } - - onTextChange(text: string) { - _.assign(_.extend(this.props.clipperState.pdfPreviewInfo, { - selectedPageRange: text - } as PdfPreviewInfo), this.props.clipperState.setState); - } - - onTextInputFocus(): void { - if (this.props.clipperState.pdfPreviewInfo.shouldShowPopover) { - _.assign(_.extend(this.props.clipperState.pdfPreviewInfo, { - shouldShowPopover: false - } as PdfPreviewInfo), this.props.clipperState.setState); - } - } - - onSelectionChange(selection: boolean) { - _.assign(_.extend(this.props.clipperState.pdfPreviewInfo, { - allPages: selection, - shouldShowPopover: false - } as PdfPreviewInfo), this.props.clipperState.setState); - - document.getElementById(selection ? Constants.Ids.radioAllPagesLabel : Constants.Ids.rangeInput).focus(); - } - - private getErrorMessageForInvalidPageRange(): string { - const pdfPreviewInfo = this.props.clipperState.pdfPreviewInfo; - let parsePageRangeOperation = StringUtils.parsePageRange(pdfPreviewInfo.selectedPageRange, this.props.clipperState.pdfResult.data.get().pdf.numPages()); - if (parsePageRangeOperation.status === OperationResult.Succeeded) { - throw Error("Given that shouldShowPopover is true, parsing the pageRange should never succeed: PageRange: " + pdfPreviewInfo.selectedPageRange); - } - - return Localization.getLocalizedString("WebClipper.Popover.PdfInvalidPageRange").replace("{0}", parsePageRangeOperation.result as string); - } - - getRadioButtons(): RadioButtonGroup { - let pdfPreviewInfo = this.props.clipperState.pdfPreviewInfo; - let invalidClassName = pdfPreviewInfo.shouldShowPopover ? "invalid" : ""; - let selectedTabIndex = 60; - let unselectedTabIndex = -1; - - return { - role: "radiogroup", - isAriaSet: true, - innerElements: [ -
    -
    - {pdfPreviewInfo.allPages ?
    : undefined} -
    -
    - {Localization.getLocalizedString("WebClipper.Label.PdfAllPagesRadioButton")} -
    -
    , -
    -
    - {!pdfPreviewInfo.allPages ? -
    : undefined} -
    - - {pdfPreviewInfo.shouldShowPopover ? - : undefined} -
    - ] - }; - } - - render() { - let renderables = []; - let buttonGroups = this.getRadioButtonGroups(); - - for (let i = 0; i < buttonGroups.length; i++) { - let currentButtonGroup = buttonGroups[i]; - let role = currentButtonGroup.role; - let isAriaSet = currentButtonGroup.isAriaSet; - if (isAriaSet) { - let setSize = currentButtonGroup.innerElements.length; - for (let j = 0; j < setSize; j++) { - currentButtonGroup.innerElements[j].attrs["aria-posinset"] = j + 1; - currentButtonGroup.innerElements[j].attrs["aria-setsize"] = setSize; - } - - } - renderables.push( -
    - {currentButtonGroup.innerElements} -
    ); - } - - return ( -
    - {renderables} -
    - ); - } -} - -let component = PdfPageSelectionRadioButton.componentize(); -export {component as PdfPageSelectionRadioButton}; diff --git a/src/scripts/clipperUI/components/popover.tsx b/src/scripts/clipperUI/components/popover.tsx deleted file mode 100644 index 9c372a78..00000000 --- a/src/scripts/clipperUI/components/popover.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import {Constants} from "../../constants"; -import {StringUtils} from "../../stringUtils"; - -import {ComponentBase} from "../componentBase"; - -import * as popperJS from "popper.js"; - -export interface PopoverProps { - referenceElementId: string; - placement: string; // TODO: use a union type of allowed values - parentId?: string; - content?: string; - classNames?: string[]; - arrowClassNames?: string[]; - modifiersIgnored?: string[]; // TODO: use a union type of allowed values - removeOnDestroy?: boolean; -} - -class PopoverClass extends ComponentBase<{}, PopoverProps> { - private refToPopper: popperJS; - - constructor(props: any) { - super(props); - } - - handlePopoverLifecycle(element, isInitialized, context) { - if (!isInitialized) { - let popperElement = this.generatePopperElement(this.props.parentId); - - // TODO temporarily typed this way until definitions is updated for popperJS.PopperOptions - let popperOptions: any = { - placement: this.props.placement, - removeOnDestroy: this.props.removeOnDestroy, - modifiers: {} - }; - if (this.props.modifiersIgnored) { - for (let i = 0; i < this.props.modifiersIgnored.length; i++) { - popperOptions.modifiers[this.props.modifiersIgnored[i]] = { enabled: false }; - } - } - - this.refToPopper = new popperJS(document.getElementById(this.props.referenceElementId), popperElement, popperOptions); - } - - if (isInitialized) { - if (this.refToPopper) { - this.refToPopper.update(); - } - } - - context.onunload = () => { - if (this.refToPopper) { - this.refToPopper.destroy(); - this.refToPopper = undefined; - } - }; - } - - private generatePopperElement(parentId: string): HTMLDivElement { - let popperElement = document.createElement("div") as HTMLDivElement; - popperElement.innerText = this.props.content; - - if (this.props.classNames) { - for (let i = 0; i < this.props.classNames.length; i++) { - popperElement.classList.add(this.props.classNames[i]); - } - } - - if (this.props.arrowClassNames) { - let arrowElement = document.createElement("div"); - for (let i = 0; i < this.props.arrowClassNames.length; i++) { - arrowElement.classList.add(this.props.arrowClassNames[i]); - } - arrowElement.setAttribute("x-arrow", ""); - popperElement.appendChild(arrowElement); - } - - let parent = parentId ? document.getElementById(parentId) : undefined; - if (parent) { - // We want to set the parent lower in the HTML hierarchy to avoid z-index issues relating to stacking contexts - parent.appendChild(popperElement); - } else { - document.body.appendChild(popperElement); - } - - return popperElement; - } - - render() { - return ( -
    - ); - } -} - -let component = PopoverClass.componentize(); -export {component as Popover}; diff --git a/src/scripts/clipperUI/components/previewViewer/augmentationPreview.tsx b/src/scripts/clipperUI/components/previewViewer/augmentationPreview.tsx deleted file mode 100644 index eccd4c3e..00000000 --- a/src/scripts/clipperUI/components/previewViewer/augmentationPreview.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import {Constants} from "../../../constants"; - -import {AugmentationModel, AugmentationResult} from "../../../contentCapture/augmentationHelper"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp, DataResult} from "../../clipperState"; -import {Status} from "../../status"; - -import {SpriteAnimation} from "../../components/spriteAnimation"; - -import {EditorPreviewComponentBase, EditorPreviewState} from "./editorPreviewComponentBase"; - -class AugmentationPreview extends EditorPreviewComponentBase { - protected getHighlightableContentBodyForCurrentStatus(): any[] { - let state = this.props.clipperState; - return this.convertAugmentationResultToContentData(state.augmentationResult); - } - - protected getStatus(): Status { - return this.props.clipperState.augmentationResult.status; - } - - protected getTitleTextForCurrentStatus(): string { - let noContentFoundString = Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - let failureMessage: string; - - let previewStatus = this.getStatus(); - - switch (previewStatus) { - case Status.Succeeded: - if (!this.props.clipperState.augmentationResult.data || this.props.clipperState.augmentationResult.data.ContentModel === AugmentationModel.None) { - return Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - } - return this.props.clipperState.previewGlobalInfo.previewTitleText; - case Status.NotStarted: - case Status.InProgress: - return Localization.getLocalizedString("WebClipper.Preview.LoadingMessage"); - default: - case Status.Failed: - failureMessage = this.props.clipperState.augmentationResult.data.failureMessage; - return !!failureMessage ? failureMessage : noContentFoundString; - } - } - - protected handleBodyChange(newBodyHtml: string) { - this.props.clipperState.setState({ - augmentationPreviewInfo: { previewBodyHtml: newBodyHtml } - }); - } - - private convertAugmentationResultToContentData(result: DataResult): any { - switch (result.status) { - case Status.Succeeded: - if (this.props.clipperState.augmentationResult.data && this.props.clipperState.augmentationResult.data.ContentModel !== AugmentationModel.None) { - return m.trust(this.props.clipperState.augmentationPreviewInfo.previewBodyHtml); - } - break; - case Status.NotStarted: - case Status.InProgress: - return this.getSpinner(); - default: - case Status.Failed: - break; - } - - return undefined; - } - - private getSpinner(): any { - let spinner = ; - - return
    {spinner}
    ; - } -} - -let component = AugmentationPreview.componentize(); -export {component as AugmentationPreview}; diff --git a/src/scripts/clipperUI/components/previewViewer/bookmarkPreview.tsx b/src/scripts/clipperUI/components/previewViewer/bookmarkPreview.tsx deleted file mode 100644 index 328e4b7c..00000000 --- a/src/scripts/clipperUI/components/previewViewer/bookmarkPreview.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import {Constants} from "../../../constants"; -import {ObjectUtils} from "../../../objectUtils"; - -import {BookmarkResult} from "../../../contentCapture/bookmarkHelper"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp, DataResult} from "../../clipperState"; -import {Status} from "../../status"; - -import {SpriteAnimation} from "../../components/spriteAnimation"; - -import {PreviewComponentBase} from "./previewComponentBase"; -import {PreviewViewerBookmarkHeader} from "./previewViewerBookmarkHeader"; - -class BookmarkPreview extends PreviewComponentBase<{}, ClipperStateProp> { - protected getStatus(): Status { - return this.props.clipperState.bookmarkResult.status; - } - - protected getTitleTextForCurrentStatus(): string { - let failureMessage: string; - - let defaultFailureMessage = Localization.getLocalizedString("WebClipper.Preview.BookmarkModeGenericError"); - let previewStatus = this.getStatus(); - - switch (previewStatus) { - case Status.Succeeded: - return this.props.clipperState.previewGlobalInfo.previewTitleText; - case Status.NotStarted: - case Status.InProgress: - return Localization.getLocalizedString("WebClipper.Preview.LoadingMessage"); - default: - case Status.Failed: - failureMessage = this.props.clipperState.bookmarkResult.data.failureMessage; - return !!failureMessage ? failureMessage : defaultFailureMessage; - } - } - - protected getContentBodyForCurrentStatus(): any[] { - let previewStatus = this.getStatus(); - - switch (previewStatus) { - case Status.Succeeded: - return this.convertBookmarkResultToContentData(this.props.clipperState.bookmarkResult.data); - case Status.NotStarted: - case Status.InProgress: - return [this.getSpinner()]; - default: - case Status.Failed: - return []; - } - } - - protected getSpinner(): any { - let spinner = ; - return
    {spinner}
    ; - } - - protected getHeader(): any { - return ; - } - - // Override - protected getPreviewContentContainerClass(): string { - return Constants.Ids.bookmarkPreviewContentContainer; - } - - // Override - protected getPreviewInnerContainerClass(): string { - return Constants.Ids.bookmarkPreviewInnerContainer; - } - - private convertBookmarkResultToContentData(result: BookmarkResult): any { - let url = result.url; - - let secondColumnTdStyle = ""; - if (!ObjectUtils.isNullOrUndefined(result.thumbnailSrc)) { - secondColumnTdStyle += "padding-left:16px;"; - } - - let urlTdStyle = "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:2px;"; - if (!ObjectUtils.isNullOrUndefined(result.description)) { - urlTdStyle += "padding-bottom:13px;"; - } - - return ( - - - { this.renderThumbnailIfExists(result.thumbnailSrc) } - - -
    - - { this.renderTitleIfExists(result.title) } - - - - { this.renderDescriptionIfExists(result.description) } -
    - {url} -
    -
    - ); - } - - private renderTitleIfExists(title: string) { - if (!ObjectUtils.isNullOrUndefined(title) && title.length > 0) { - return ( - - -

    {title}

    - - - ); - } - } - - private renderDescriptionIfExists(description: string) { - if (!ObjectUtils.isNullOrUndefined(description) && description.length > 0) { - return ( - - {description} - - ); - } - } - - private renderThumbnailIfExists(thumbnailSrc: string) { - if (!ObjectUtils.isNullOrUndefined(thumbnailSrc) && thumbnailSrc.length > 0) { - let thumbnailSize = "112"; - return ( - - thumbnail - - ); - } - } -} - -let component = BookmarkPreview.componentize(); -export {component as BookmarkPreview}; diff --git a/src/scripts/clipperUI/components/previewViewer/editorPreviewComponentBase.tsx b/src/scripts/clipperUI/components/previewViewer/editorPreviewComponentBase.tsx deleted file mode 100644 index 3570d966..00000000 --- a/src/scripts/clipperUI/components/previewViewer/editorPreviewComponentBase.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import {Constants} from "../../../constants"; -import {PreviewGlobalInfo} from "../../../previewInfo"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {Highlighter} from "../../../highlighting/highlighter"; - -import {ClipMode} from "../../clipMode"; -import {ClipperStateProp} from "../../clipperState"; -import {Status} from "../../status"; - -import {PreviewComponentBase} from "./previewComponentBase"; -import {PreviewViewerAugmentationHeader} from "./previewViewerAugmentationHeader"; - -import * as _ from "lodash"; -import { Localization } from "../../../localization/localization"; -import { Clipper } from "../../frontEndGlobals"; -import * as Log from "../../../logging/log"; - -export interface EditorPreviewState { - textHighlighter?: any; -} - -/** - * Child components will inherit the editor header where users can make rich edits to their content, such as highlighting - * and font changes. Within the preview body element, this component renders a highlightable preview body element underneath - * it that the highlighter is attached to. Highlighting logic can only take place within this element, and this prevents - * other types of preview bodies from accidentally providing highlighting functionality. - */ -export abstract class EditorPreviewComponentBase - extends PreviewComponentBase { - - // We use this to force the existence of only one click handler - private static currentClickHandler: EventListener; - private clickHandler = this.handleClick.bind(this); - - constructor(props: TProps) { - super(props); - - // This is to make sure we cleanly override the old click handler before we attach the current child's one - if (EditorPreviewComponentBase.currentClickHandler) { - window.removeEventListener("click", EditorPreviewComponentBase.currentClickHandler); - } - - // We have to do it this way as we lose old onclick event listeners when we try and attach them to buttons individually - window.addEventListener("click", this.clickHandler); - EditorPreviewComponentBase.currentClickHandler = this.clickHandler; - } - - protected abstract handleBodyChange(newBodyHtml: string); - protected abstract getHighlightableContentBodyForCurrentStatus(): any; - - // Override - protected getContentBodyForCurrentStatus() { - return [ -
    {this.getHighlightableContentBodyForCurrentStatus()}
    - ]; - } - - // Makes all interactive elements (links, buttons) within the article body non-tabbable - private makeChildLinksNonTabbable(element: HTMLElement, isInitialized: boolean) { - if (!isInitialized) { - return; - } - const interactiveElements = element.querySelectorAll("a, button, input, select, textarea"); - interactiveElements.forEach((el: HTMLElement) => { - el.tabIndex = -1; - }); - } - - // Override - protected getPreviewBodyConfig() { - if (!this.state.textHighlighter) { - this.setHighlighter(); - } - if (this.props.clipperState.previewGlobalInfo.highlighterEnabled) { - this.state.textHighlighter.enable(); - } else { - this.state.textHighlighter.disable(); - } - } - - protected getHeader() { - return ; - } - - // Override - protected getPreviewBodyClass(): string { - return this.state.textHighlighter && this.state.textHighlighter.isEnabled() ? Constants.Classes.highlightable : ""; - } - - private announceWithAriaLive(announcement: string) { - const ariaLiveDiv = document.getElementById(Constants.Ids.previewAriaLiveDiv); - if (!ariaLiveDiv) { - Clipper.logger.logTrace(Log.Trace.Label.General, Log.Trace.Level.Warning, `Aria-live div with id ${Constants.Ids.sectionLocationContainer} not found`); - return; - } - // To make duplicate text announcement work. See https://core.trac.wordpress.org/ticket/36853 - if (ariaLiveDiv.textContent === announcement) { - announcement += " \u00A0"; - } - ariaLiveDiv.textContent = announcement; - } - - private changeFontFamily(serif: boolean) { - _.assign(_.extend(this.props.clipperState.previewGlobalInfo, { - serif: serif - } as PreviewGlobalInfo), this.props.clipperState.setState); - - if (serif) { - this.announceWithAriaLive(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.ChangeFontToSerif")); - } else { - this.announceWithAriaLive(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.ChangeFontToSansSerif")); - } - } - - private changeFontSize(increase: boolean) { - let newFontSize: number = this.props.clipperState.previewGlobalInfo.fontSize + (increase ? 2 : -2); - if (newFontSize < Constants.Settings.minimumFontSize) { - newFontSize = Constants.Settings.minimumFontSize; - } else if (newFontSize > Constants.Settings.maximumFontSize) { - newFontSize = Constants.Settings.maximumFontSize; - } - - _.assign(_.extend(this.props.clipperState.previewGlobalInfo, { - fontSize: newFontSize - } as PreviewGlobalInfo), this.props.clipperState.setState); - - if (increase) { - this.announceWithAriaLive(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.IncreaseFontSize")); - } else { - this.announceWithAriaLive(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.DecreaseFontSize")); - } - } - - private deleteHighlight(timestamp: number) { - let highlightablePreviewBody = document.getElementById(Constants.Ids.highlightablePreviewBody); - let highlightedElements = highlightablePreviewBody.querySelectorAll("span.highlighted[data-timestamp='" + timestamp + "']"); - for (let i = 0; i < highlightedElements.length; i++) { - let current = highlightedElements[i] as HTMLSpanElement; - let parent = current.parentNode; - parent.insertBefore(document.createTextNode(current.innerText), current); - parent.removeChild(current); - } - this.handleBodyChange(highlightablePreviewBody.innerHTML); - } - - private handleClick(event: Event) { - if (event && event.target) { - let element = event.target as HTMLElement; - if (element.className && element.className.indexOf(Constants.Classes.deleteHighlightButton) !== -1) { - this.deleteHighlight(parseInt(element.getAttribute("data-timestamp"), 10)); - - // If the button lives inside an anchor, we don't want to trigger that anchor if the button was clicked - event.preventDefault(); - } - } - } - - private setHighlighter() { - let addDeleteButton = (range: Range, normalizedHighlights: HTMLSpanElement[]) => { - if (normalizedHighlights && normalizedHighlights.length > 0) { - let highlightablePreviewBody = document.getElementById(Constants.Ids.highlightablePreviewBody); - - // We need to get the latest timestamp for normalizing all encompassed highlights later - let timestamps = normalizedHighlights.map((span: HTMLSpanElement) => parseInt(span.getAttribute("data-timestamp"), 10 /* radix */)); - let timestamp: number = Math.max.apply(undefined, timestamps); - - // The highlight may have intersected another highlight ... - for (let i = 0; i < normalizedHighlights.length; i++) { - // ... so we should delete their old delete buttons, and normalize them to the same timestamp - let oldHighlightTimestamp = normalizedHighlights[i].getAttribute("data-timestamp"); - let oldHighlights = highlightablePreviewBody.querySelectorAll("span." + Constants.Classes.highlighted + "[data-timestamp='" + oldHighlightTimestamp + "']"); - for (let j = 0; j < oldHighlights.length; j++) { - // Delete old delete buttons - let oldButtons = oldHighlights[j].querySelectorAll("img." + Constants.Classes.deleteHighlightButton); - for (let k = 0; k < oldButtons.length; k++) { - oldButtons[k].parentNode.removeChild(oldButtons[k]); - } - - // Normalize timestamp - oldHighlights[j].setAttribute("data-timestamp", "" + timestamp); - } - } - - // Find the first instance of the highlight and add the delete button - let firstHighlighted = highlightablePreviewBody.querySelector("span.highlighted[data-timestamp='" + timestamp + "']"); - if (firstHighlighted) { - let deleteHighlight = document.createElement("img") as HTMLImageElement; - deleteHighlight.src = ExtensionUtils.getImageResourceUrl("editoroptions/delete_button.svg"); - deleteHighlight.className = Constants.Classes.deleteHighlightButton; - deleteHighlight.setAttribute("data-timestamp", "" + timestamp); - deleteHighlight.setAttribute("tabindex", "0"); - firstHighlighted.insertBefore(deleteHighlight, firstHighlighted.childNodes[0]); - } - - this.handleBodyChange(highlightablePreviewBody.innerHTML); - } - }; - - let textHighlighter = Highlighter.reconstructInstance(document.getElementById(Constants.Ids.highlightablePreviewBody), { - color: Constants.Styles.Colors.oneNoteHighlightColor, - contextClass: Constants.Classes.highlightable, - onAfterHighlight: addDeleteButton - }); - - this.setState({ - textHighlighter: textHighlighter - } as any); - } - - private toggleHighlight() { - if (!this.props.clipperState.previewGlobalInfo.highlighterEnabled && !window.getSelection().isCollapsed && this.selectionIsInPreviewBody()) { - // If the user selects something and clicks the highlighter button, we behave traditionally (i.e., perform highlighting, not toggling) - this.state.textHighlighter.doHighlight(); - } else { - // No selection found, so we actually toggle the highlighter functionality - _.assign(_.extend(this.props.clipperState.previewGlobalInfo, { - highlighterEnabled: !this.props.clipperState.previewGlobalInfo.highlighterEnabled - } as PreviewGlobalInfo), this.props.clipperState.setState); - } - } - - // Similarly adopted from: http://stackoverflow.com/questions/8339857/how-to-know-if-selected-text-is-inside-a-specific-div - private selectionIsInPreviewBody() { - let previewBody = document.getElementById(Constants.Ids.highlightablePreviewBody); - if (!previewBody) { - return false; - } - - let selection = window.getSelection(); - if (selection.rangeCount > 0) { - // Check that all range parts belong to the preview body - for (let i = 0; i < selection.rangeCount; i++) { - let commonAncestorContainer = selection.getRangeAt(i).commonAncestorContainer; - if (!(commonAncestorContainer === previewBody || previewBody.contains(commonAncestorContainer))) { - return false; - } - } - return true; - } - - return false; - } -} diff --git a/src/scripts/clipperUI/components/previewViewer/fullPagePreview.tsx b/src/scripts/clipperUI/components/previewViewer/fullPagePreview.tsx deleted file mode 100644 index cf56ea8e..00000000 --- a/src/scripts/clipperUI/components/previewViewer/fullPagePreview.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import {Constants} from "../../../constants"; - -import {SmartValue} from "../../../communicator/smartValue"; - -import {FullPageScreenshotResult} from "../../../contentCapture/fullPageScreenshotHelper"; -import {PdfScreenshotResult} from "../../../contentCapture/pdfScreenshotHelper"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp, DataResult} from "../../clipperState"; -import {Status} from "../../status"; - -import {RotatingMessageSpriteAnimation} from "../../components/rotatingMessageSpriteAnimation"; - -import {PreviewComponentBase} from "./previewComponentBase"; -import {PreviewViewerFullPageHeader} from "./previewViewerFullPageHeader"; - -class FullPagePreview extends PreviewComponentBase<{}, ClipperStateProp> { - protected getContentBodyForCurrentStatus(): any[] { - let state = this.props.clipperState; - - if (!state.pageInfo) { - return [this.getSpinner()]; - } - - return this.convertFullPageResultToContentData(state.fullPageResult); - } - - protected getHeader(): any { - return ; - } - - protected getStatus(): Status { - if (!this.props.clipperState.pageInfo) { - return Status.NotStarted; - } - - return this.props.clipperState.fullPageResult.status; - } - - protected getTitleTextForCurrentStatus(): string { - let noContentFoundString = Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - let failureMessage: string; - - let previewStatus = this.getStatus(); - let pageInfo = this.props.clipperState.pageInfo; - - switch (previewStatus) { - case Status.Succeeded: - if (pageInfo && !this.props.clipperState.fullPageResult.data) { - return Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - } - return this.props.clipperState.previewGlobalInfo.previewTitleText; - case Status.NotStarted: - case Status.InProgress: - return Localization.getLocalizedString("WebClipper.Preview.LoadingMessage"); - default: - case Status.Failed: - failureMessage = this.props.clipperState.fullPageResult.data.failureMessage; - return !!failureMessage ? failureMessage : noContentFoundString; - } - } - - private convertFullPageResultToContentData(result: DataResult): any[] { - let contentBody = []; - - switch (result.status) { - case Status.Succeeded: - let pageTitle = this.props.clipperState.pageInfo ? this.props.clipperState.pageInfo.contentTitle : ""; - let altTag = Localization.getLocalizedString("WebClipper.Preview.FullPageModeScreenshotDescription").replace("{0}", pageTitle); - - if (this.props.clipperState.fullPageResult.data) { - let screenshotImages: FullPageScreenshotResult = this.props.clipperState.fullPageResult.data; - for (let imageData of screenshotImages.Images) { - let dataUrl = "data:image/" + screenshotImages.ImageFormat + ";" + screenshotImages.ImageEncoding + "," + imageData; - contentBody.push({altTag}); - } - } - break; - case Status.NotStarted: - case Status.InProgress: - contentBody.push(this.getSpinner()); - break; - default: - case Status.Failed: - break; - } - - return contentBody; - } - - private getSpinner(): any { - let spinner = ; - return
    {spinner}
    ; - } -} - -let component = FullPagePreview.componentize(); -export {component as FullPagePreview}; diff --git a/src/scripts/clipperUI/components/previewViewer/localFilesNotAllowedPanel.tsx b/src/scripts/clipperUI/components/previewViewer/localFilesNotAllowedPanel.tsx deleted file mode 100644 index 8af1be2c..00000000 --- a/src/scripts/clipperUI/components/previewViewer/localFilesNotAllowedPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {Constants} from "../../../constants"; - -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp} from "../../clipperState"; -import {ComponentBase} from "../../componentBase"; -import {Status} from "../../status"; - -import {PreviewComponentBase} from "./previewComponentBase"; - -interface LocalFilesNotAllowedPanelProps { - header?: string; - title?: string; - subtitle?: string; -} - -export class LocalFilesNotAllowedPanelClass extends ComponentBase<{}, LocalFilesNotAllowedPanelProps> { - render() { - return ( -
    -
    -
    -
    -
    - - {this.props.header} - -
    -
    -
    -
    {this.props.title}
    -
    {this.props.subtitle}
    -
    -
    -
    -
    - ); - } -} - -let component = LocalFilesNotAllowedPanelClass.componentize(); -export {component as LocalFilesNotAllowedPanel}; diff --git a/src/scripts/clipperUI/components/previewViewer/pdfPageViewport.tsx b/src/scripts/clipperUI/components/previewViewer/pdfPageViewport.tsx deleted file mode 100644 index 5736cf46..00000000 --- a/src/scripts/clipperUI/components/previewViewer/pdfPageViewport.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import {Constants} from "../../../constants"; - -import {ComponentBase} from "../../componentBase"; -import {SpriteAnimation} from "../../components/spriteAnimation"; - -import {ViewportDimensions} from "../../../contentCapture/viewportDimensions"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -export interface PdfPageViewportProp { - viewportDimensions: ViewportDimensions; - imgUrl?: string; - index: number; -} - -class PdfPageViewportClass extends ComponentBase<{}, PdfPageViewportProp> { - private getViewportStyle(): string { - let styleString = "max-width: " + this.props.viewportDimensions.width + "px;"; - return styleString + "max-height: " + this.props.viewportDimensions.height + "px;"; - } - - private getPlaceholderStyle(): string { - return "padding-bottom: " + ((this.props.viewportDimensions.height / this.props.viewportDimensions.width) * 100) + "%;"; - } - - private getSpinner(): any { - let spinner = ; - - return
    {spinner}
    ; - } - - public render() { - return ( -
    - {this.props.imgUrl ? - : -
    {this.getSpinner()}
    } -
    - ); - } - -} - -let component = PdfPageViewportClass.componentize(); -export {component as PdfPageViewport}; diff --git a/src/scripts/clipperUI/components/previewViewer/pdfPreview.tsx b/src/scripts/clipperUI/components/previewViewer/pdfPreview.tsx deleted file mode 100644 index 444751f8..00000000 --- a/src/scripts/clipperUI/components/previewViewer/pdfPreview.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import {Constants} from "../../../constants"; -import {ObjectUtils} from "../../../objectUtils"; -import {OperationResult} from "../../../operationResult"; -import {PdfPreviewInfo} from "../../../previewInfo"; -import {StringUtils} from "../../../stringUtils"; -import {UrlUtils} from "../../../urlUtils"; - -import {SmartValue} from "../../../communicator/smartValue"; - -import {FullPageScreenshotResult} from "../../../contentCapture/fullPageScreenshotHelper"; -import {PdfScreenshotHelper, PdfScreenshotResult} from "../../../contentCapture/pdfScreenshotHelper"; - -import {DomUtils} from "../../../domParsers/domUtils"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {Localization} from "../../../localization/localization"; - -import {ClipMode} from "../../clipMode"; -import {ClipperStateProp, DataResult} from "../../clipperState"; -import {Status} from "../../status"; - -import {RotatingMessageSpriteAnimation} from "../../components/rotatingMessageSpriteAnimation"; - -import {PdfPreviewAttachment} from "./pdfPreviewAttachment"; -import {PdfPreviewPage} from "./pdfPreviewPage"; -import {PreviewComponentBase} from "./previewComponentBase"; -import {PreviewViewerPdfHeader} from "./previewViewerPdfHeader"; - -import * as _ from "lodash"; - -type IndexToDataUrlMap = { [index: number]: string; } - -interface PdfPreviewState { - showPageNumbers?: boolean; - renderedPageIndexes?: IndexToDataUrlMap; -} - -class PdfPreviewClass extends PreviewComponentBase { - private static latestScrollListener: (event: UIEvent) => void; - private static scrollListenerTimeout: number; - private initPageRenderCalled: boolean = false; - - constructor(props: ClipperStateProp) { - super(props); - // We need to do this on every constructor to ensure the reference to the state - // object is correct - this.addScrollListener(); - } - - getInitialState(): PdfPreviewState { - return { - showPageNumbers: false, - renderedPageIndexes: {}, - }; - } - - private searchForVisiblePageBoundary(allPages: NodeListOf, initPageIndex: number, incrementer: number): number { - let pageIndexToTest = initPageIndex; - let guessAtPageBoundary = allPages[pageIndexToTest] as HTMLDivElement; - - while (guessAtPageBoundary && this.pageIsVisible(guessAtPageBoundary)) { - pageIndexToTest += incrementer; - guessAtPageBoundary = allPages[pageIndexToTest] as HTMLDivElement; - } - - // result of adding last incrementer was a non-visible page, so return the page num from before that - return Math.max(pageIndexToTest -= incrementer, 0); - } - - /** - * Get an approximation of the centermost visible page currently in the viewport (based on scroll percentage). - * If the page is indeed in the viewport, search up and down for the visible page boundaries. - * If the page approximation was incorrect and the page is not visible, begin a fan-out search of the area around the approximate page - * for a visible page. When a visible page boundary is found, find the other visible page boundary. - */ - private getIndicesOfVisiblePages(): number[] { - let allPages = document.querySelectorAll("div[data-pageindex]"); - - const initGuessAtCurrentPageIndexer: number = Math.floor(DomUtils.getScrollPercent(document.getElementById("previewContentContainer"), true /* asDecimalValue */) * (allPages.length - 1)); - - let firstVisiblePageIndexer: number; - let lastVisiblePageIndexer: number; - if (this.pageIsVisible(allPages[initGuessAtCurrentPageIndexer] as HTMLDivElement)) { - firstVisiblePageIndexer = this.searchForVisiblePageBoundary(allPages, initGuessAtCurrentPageIndexer, -1); - lastVisiblePageIndexer = this.searchForVisiblePageBoundary(allPages, initGuessAtCurrentPageIndexer, 1); - } else { - let incrementer = 1; - let guessAtVisiblePageIndexer = initGuessAtCurrentPageIndexer + incrementer; - let guessAtVisiblePageBoundary = allPages[guessAtVisiblePageIndexer] as HTMLDivElement; - - while (!this.pageIsVisible(guessAtVisiblePageBoundary)) { - if (incrementer > 0) { - incrementer *= -1; - } else { - incrementer = (incrementer * -1) + 1; - } - - guessAtVisiblePageIndexer = initGuessAtCurrentPageIndexer + incrementer; - - guessAtVisiblePageBoundary = allPages[guessAtVisiblePageIndexer] as HTMLDivElement; - } - - if (incrementer > 0) { - firstVisiblePageIndexer = guessAtVisiblePageIndexer; - lastVisiblePageIndexer = this.searchForVisiblePageBoundary(allPages, firstVisiblePageIndexer, 1); - } else { - lastVisiblePageIndexer = guessAtVisiblePageIndexer; - firstVisiblePageIndexer = this.searchForVisiblePageBoundary(allPages, lastVisiblePageIndexer, -1); - } - } - - // _.range does not include the end number, so add 1 - return _.range(firstVisiblePageIndexer, lastVisiblePageIndexer + 1); - } - - private setDataUrlsOfImagesInViewportInState() { - let pageIndicesToRender: number[] = this.getIndicesOfVisiblePages(); - - // Pad pages to each end of the list to increase the scroll distance before the user hits a blank page - if (pageIndicesToRender.length > 0) { - let first = pageIndicesToRender[0]; - let extraPagesToPrepend = _.range(Math.max(first - Constants.Settings.pdfExtraPageLoadEachSide, 0), first); - - let afterLast = pageIndicesToRender[pageIndicesToRender.length - 1] + 1; - let extraPagesToAppend = _.range(afterLast, Math.min(afterLast + Constants.Settings.pdfExtraPageLoadEachSide, this.props.clipperState.pdfResult.data.get().pdf.numPages())); - - pageIndicesToRender = extraPagesToPrepend.concat(pageIndicesToRender).concat(extraPagesToAppend); - } - - this.setDataUrlsOfImagesInState(pageIndicesToRender); - } - - private pageIsVisible(element: HTMLElement): boolean { - if (!element) { - return false; - } - - let rect = element.getBoundingClientRect(); - return rect.top <= window.innerHeight && rect.bottom >= 0; - } - - private setDataUrlsOfImagesInState(pageIndicesToRender: number[]) { - this.props.clipperState.pdfResult.data.get().pdf.getPageListAsDataUrls(pageIndicesToRender).then((dataUrls) => { - let renderedIndexes: IndexToDataUrlMap = {}; - for (let i = 0; i < dataUrls.length; i++) { - renderedIndexes[pageIndicesToRender[i]] = dataUrls[i]; - } - this.setState({ - renderedPageIndexes: renderedIndexes - }); - }); - } - - /** - * Gets the page components to be rendered in the preview - */ - private getPageComponents(): any[] { - let pages = []; - let pdfResult = this.props.clipperState.pdfResult.data.get(); - - // Determine which pages should be marked as selected vs unselected - let pagesToShow: number[]; - let parsePageRangeOperation = StringUtils.parsePageRange(this.props.clipperState.pdfPreviewInfo.selectedPageRange); - if (parsePageRangeOperation.status !== OperationResult.Succeeded) { - pagesToShow = []; - } else { - // If the operation Succeeded, the result should always be a number[] - pagesToShow = parsePageRangeOperation.result as number[]; - } - pagesToShow = pagesToShow.map((ind) => { return ind - 1; }); - - for (let i = 0; i < pdfResult.pdf.numPages(); i++) { - pages.push(= 0} - viewportDimensions={pdfResult.viewportDimensions[i]} imgUrl={this.state.renderedPageIndexes[i]} index={i} />); - } - return pages; - } - - private addScrollListener() { - if (PdfPreviewClass.latestScrollListener) { - window.removeEventListener("scroll", PdfPreviewClass.latestScrollListener, true); - } - // When we detect a scroll, show page numbers immediately. - // When the user doesn't scroll for some period of time, fade them out. - PdfPreviewClass.latestScrollListener = (event) => { - let element = event.target as HTMLElement; - if (!!element && element.id === Constants.Ids.previewContentContainer) { - if (ObjectUtils.isNumeric(PdfPreviewClass.scrollListenerTimeout)) { - clearTimeout(PdfPreviewClass.scrollListenerTimeout); - } - PdfPreviewClass.scrollListenerTimeout = setTimeout(() => { - this.setState({ - showPageNumbers: false - }); - // We piggyback the scroll listener to determine what pages the user is looking at, then render them - if (this.props.clipperState.currentMode.get() === ClipMode.Pdf && this.props.clipperState.pdfResult.status === Status.Succeeded) { - this.setDataUrlsOfImagesInViewportInState(); - } - }, Constants.Settings.timeUntilPdfPageNumbersFadeOutAfterScroll); - - // A little optimization to prevent us from calling render a large number of times - if (!this.state.showPageNumbers) { - this.setState({ - showPageNumbers: true - }); - } - } - }; - // TODO does this work on touch and pageup/down too? - window.addEventListener("scroll", PdfPreviewClass.latestScrollListener, true /* allows the listener to listen to all elements */); - } - - protected getContentBodyForCurrentStatus(): any[] { - let state = this.props.clipperState; - if (state.pdfResult.status === Status.InProgress || state.pdfResult.status === Status.NotStarted) { - return [this.getSpinner()]; - } - - return this.convertPdfResultToContentData(state.pdfResult); - } - - protected getHeader(): any { - return ; - } - - protected getStatus(): Status { - if (!this.props.clipperState.pageInfo) { - return Status.NotStarted; - } - return this.props.clipperState.pdfResult.status; - } - - protected getTitleTextForCurrentStatus(): string { - let noContentFoundString = Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - let failureMessage: string; - - let previewStatus = this.getStatus(); - let pdfResult = this.props.clipperState.pdfResult; - switch (previewStatus) { - case Status.Succeeded: - // TODO: verify this is actually what happens - if (pdfResult && !pdfResult.data.get()) { - return Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - } - return this.props.clipperState.previewGlobalInfo.previewTitleText; - case Status.NotStarted: - case Status.InProgress: - return Localization.getLocalizedString("WebClipper.Preview.LoadingMessage"); - default: - case Status.Failed: - failureMessage = this.props.clipperState.pdfResult.data.get().failureMessage; - return !!failureMessage ? failureMessage : noContentFoundString; - } - } - - private convertPdfResultToContentData(result: DataResult>): any[] { - let contentBody = []; - switch (result.status) { - case Status.Succeeded: - if (!this.initPageRenderCalled) { - // Load the first n pages (or fewer if numPages < n) as soon as we are able to - this.initPageRenderCalled = true; - this.setDataUrlsOfImagesInState(_.range(Math.min(Constants.Settings.pdfInitialPageLoadCount, result.data.get().pdf.numPages()))); - } - - // In OneNote we don't display the extension - let shouldAttachPdf = this.props.clipperState.pdfPreviewInfo.shouldAttachPdf; - let defaultAttachmentName = "Original.pdf"; - let fullAttachmentName = this.props.clipperState.pageInfo ? UrlUtils.getFileNameFromUrl(this.props.clipperState.pageInfo.rawUrl, defaultAttachmentName) : defaultAttachmentName; - if (shouldAttachPdf) { - contentBody.push(); - } - - contentBody = contentBody.concat(this.getPageComponents()); - break; - case Status.NotStarted: - case Status.InProgress: - contentBody.push(this.getSpinner()); - break; - default: - case Status.Failed: - break; - } - return contentBody; - } - - private getSpinner(): any { - let spinner = ; - return
    {spinner}
    ; - } -} - -let component = PdfPreviewClass.componentize(); -export {component as PdfPreview}; diff --git a/src/scripts/clipperUI/components/previewViewer/pdfPreviewAttachment.tsx b/src/scripts/clipperUI/components/previewViewer/pdfPreviewAttachment.tsx deleted file mode 100644 index 62488c49..00000000 --- a/src/scripts/clipperUI/components/previewViewer/pdfPreviewAttachment.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import {Constants} from "../../../constants"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {ComponentBase} from "../../componentBase"; - -export interface PdfPreviewAttachmentProp { - name: string; -} - -/** - * Provides page number overlay and selected/unselected stylings on top of the - * pdf page viewport's functionality. - */ -class PdfPreviewAttachmentClass extends ComponentBase<{}, PdfPreviewAttachmentProp> { - public render() { - return ( - - -
    {this.props.name}
    -
    - ); - } -} - -let component = PdfPreviewAttachmentClass.componentize(); -export {component as PdfPreviewAttachment}; diff --git a/src/scripts/clipperUI/components/previewViewer/pdfPreviewPage.tsx b/src/scripts/clipperUI/components/previewViewer/pdfPreviewPage.tsx deleted file mode 100644 index 3168e9aa..00000000 --- a/src/scripts/clipperUI/components/previewViewer/pdfPreviewPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import {Constants} from "../../../constants"; - -import {ComponentBase} from "../../componentBase"; - -import {PdfPageViewport, PdfPageViewportProp} from "./PdfPageViewport"; - -export interface PdfPreviewPageProp extends PdfPageViewportProp { - showPageNumber: boolean; - isSelected: boolean; -} - -/** - * Provides page number overlay and selected/unselected stylings on top of the - * pdf page viewport's functionality. - */ -class PdfPreviewPageClass extends ComponentBase<{}, PdfPreviewPageProp> { - public render() { - return ( -
    -
    - -
    -
    - {this.props.index + 1} -
    -
    - ); - } -} - -let component = PdfPreviewPageClass.componentize(); -export {component as PdfPreviewPage}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewComponentBase.tsx b/src/scripts/clipperUI/components/previewViewer/previewComponentBase.tsx deleted file mode 100644 index 905d08ed..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewComponentBase.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import * as _ from "lodash"; -import {ClientType} from "../../../clientType"; -import {Constants} from "../../../constants"; -import {Localization} from "../../../localization/localization"; -import {PreviewGlobalInfo} from "../../../previewInfo"; -import {ClipMode} from "../../clipMode"; -import {ClipperStateProp} from "../../clipperState"; -import {ComponentBase} from "../../componentBase"; -import {Status} from "../../status"; -import {AnnotationInput} from "../annotationInput"; - -export abstract class PreviewComponentBase - extends ComponentBase { - - private static textAreaListenerAttached = false; - - protected abstract getContentBodyForCurrentStatus(): any[]; - protected abstract getHeader(): any; - protected abstract getStatus(): Status; - protected abstract getTitleTextForCurrentStatus(): string; - - /** - * Returns a config callback that should be added to the preview body, and is meant to be - * overridden by child classes on a per-need basis - */ - protected getPreviewBodyConfig(): any { - return undefined; - } - - private addTextAreaListener() { - document.addEventListener("input", (event) => { - let element = event.target; - let previewHeaderInput = document.getElementById(Constants.Ids.previewHeaderInput) as HTMLTextAreaElement; - if (!!element && element === previewHeaderInput) { - this.handleTitleChange(previewHeaderInput.value); - } - }); - } - - private handleFocusOnRender(element: HTMLElement) { - if (this.props.clipperState.focusOnRender === element.id) { - element.setAttribute("tabindex", "-1"); - element.focus(); - this.props.clipperState.setState({ - focusOnRender: undefined - }); - } - } - - private handleTitleChange(newTitleText: string) { - _.assign(_.extend(this.props.clipperState.previewGlobalInfo, { - previewTitleText: newTitleText - } as PreviewGlobalInfo), this.props.clipperState.setState); - } - - private getPreviewTitle(contentTitle: string, titleIsEditable: boolean, inProgressClassIfApplicable: string): any { - if (this.props.clipperState.currentMode.get() !== ClipMode.Bookmark) { - return ( -
    -
    {contentTitle}
    - -
    - ); - } - } - - private getPreviewSubtitle(): any { - let sourceUrlCitationPrefix = Localization.getLocalizedString("WebClipper.FromCitation") - .replace("{0}", ""); // TODO can we change this loc string to remove the {0}? - - let sourceUrl = this.props.clipperState.pageInfo ? this.props.clipperState.pageInfo.rawUrl : ""; - - return ( -
    - {this.props.clipperState.injectOptions && this.props.clipperState.injectOptions.enableAddANote ? - - : undefined} - {this.props.clipperState.currentMode.get() !== ClipMode.Bookmark ? -
    - {sourceUrlCitationPrefix} - {sourceUrl} -
    - : undefined} -
    - ); - } - - /** - * Returns a class string that should be added to the preview body, and is meant to be - * overridden by child classes on a per-need basis - */ - protected getPreviewBodyClass(): string { - return ""; - } - - /** - * Returns a class string that should be added to the preview content container - * and is meant to be overriden by child classes on a per-need basis - */ - protected getPreviewContentContainerClass(): string { - return ""; - } - - /** - * Returns a class string that should be added to the preview inner container - * and is meant to be overriden by child classes on a per-need basis - */ - protected getPreviewInnerContainerClass(): string { - return ""; - } - - // Can be overriden by child classes to disable the title - protected isTitleEnabled(): boolean { - return true; - } - - render() { - if (!PreviewComponentBase.textAreaListenerAttached) { - this.addTextAreaListener(); - PreviewComponentBase.textAreaListenerAttached = true; - } - - let contentTitle = this.getTitleTextForCurrentStatus(); - let contentBody = this.getContentBodyForCurrentStatus(); - - let editableTitleEnabled = this.props.clipperState.injectOptions && this.props.clipperState.injectOptions.enableEditableTitle; - let titleIsEditable = editableTitleEnabled && this.getStatus() === Status.Succeeded && - contentTitle === this.props.clipperState.previewGlobalInfo.previewTitleText; - - let fontFamilyString = (this.props.clipperState.previewGlobalInfo.serif) ? "WebClipper.FontFamily.Preview.SerifDefault" : "WebClipper.FontFamily.Preview.SansSerifDefault"; - let previewStyle = { - fontFamily: Localization.getLocalizedString(fontFamilyString), - fontSize: this.props.clipperState.previewGlobalInfo.fontSize.toString() + "px" - }; - - let statusForCurrentMode = this.getStatus(); - let inProgressClassIfApplicable = statusForCurrentMode === Status.InProgress ? " in-progress" : ""; - - // In IE, height: auto will result in a height of 0 so we have to set it to 100% like the other modes - let previewInnerContainerClass = this.props.clipperState.clientInfo.clipperType === ClientType.Bookmarklet ? "" : this.getPreviewInnerContainerClass(); - - return ( -
    -
    -
    -
    -
    - {this.getHeader()} -
    -
    - {this.isTitleEnabled() ?
    - {this.getPreviewTitle(contentTitle, titleIsEditable, inProgressClassIfApplicable)} - {this.getPreviewSubtitle()} -
    : ""} -
    - {contentBody} -
    -
    -
    -
    -
    - ); - } -} diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerAugmentationHeader.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerAugmentationHeader.tsx deleted file mode 100644 index dfbe92e4..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerAugmentationHeader.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { SmartValue } from "../../../communicator/smartValue"; -import {Constants} from "../../../constants"; -import {ExtensionUtils} from "../../../extensions/extensionUtils"; -import {Localization} from "../../../localization/localization"; -import { ClipMode } from "../../clipMode"; -import {ControlGroup, HeaderClasses, PreviewViewerHeaderComponentBase} from "./previewViewerHeaderComponentBase"; - -export interface PreviewViewerAugmentationHeaderProp { - toggleHighlight: () => void; - changeFontFamily: (serif: boolean) => void; - changeFontSize: (increase: boolean) => void; - serif: boolean; - textHighlighterEnabled: boolean; - currentMode?: SmartValue; -} - -class PreviewViewerAugmentationHeaderClass extends PreviewViewerHeaderComponentBase<{}, PreviewViewerAugmentationHeaderProp> { - getControlGroups(): ControlGroup[] { - return [this.getTitleGroup(), this.getHighlightGroup(), this.getSerifGroup(), this.getFontSizeGroup()]; - } - - private getTitleGroupPrivate(controlGroupId: string, header: string, headerId: string): ControlGroup { - return { - id: controlGroupId, - innerElements: [ -
    - {header} -
    - ] - }; - } - - private getTitleGroup() { - let headerTitle: string = this.props.currentMode.get() === ClipMode.Selection ? - Localization.getLocalizedString("WebClipper.ClipType.Selection.Button") : - Localization.getLocalizedString("WebClipper.ClipType.Article.Button"); - return this.getTitleGroupPrivate("augmentationHeaderControlGroupId", headerTitle, "augmentationHeaderTitle"); - } - - private getHighlightGroup(): ControlGroup { - let highlighterEnabled = this.props.textHighlighterEnabled; - let classForHighlighter = highlighterEnabled ? HeaderClasses.Button.active : ""; - let imgSrc = highlighterEnabled ? "editorOptions/highlight_tool_on.svg" : "editorOptions/highlight_tool_off.svg"; - - return { - id: Constants.Ids.highlightControl, - innerElements: [ - - ] - }; - } - - private getSerifGroup(): ControlGroup { - let tabIndexOn = 101; - let tabIndexOff = -1; - return { - id: Constants.Ids.serifControl, - role: "radiogroup", - isAriaSet: true, - innerElements: [ - , - - ] - }; - } - - private getFontSizeGroup(): ControlGroup { - return { - id: Constants.Ids.fontSizeControl, - className: HeaderClasses.Button.relatedButtons, - isAriaSet: true, - innerElements: [ - , - - ] - }; - } -} - -let component = PreviewViewerAugmentationHeaderClass.componentize(); -export {component as PreviewViewerAugmentationHeader}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerBookmarkHeader.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerBookmarkHeader.tsx deleted file mode 100644 index b526250d..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerBookmarkHeader.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import {Constants} from "../../../constants"; -import {Localization} from "../../../localization/localization"; - -import {PreviewViewerTitleOnlyHeaderComponentBase} from "./previewViewerTitleOnlyHeaderComponentBase"; - -class PreviewViewerBookmarkHeaderClass extends PreviewViewerTitleOnlyHeaderComponentBase { - public getControlGroupId(): string { - return Constants.Ids.bookmarkControl; - } - - public getHeader(): string { - return Localization.getLocalizedString("WebClipper.ClipType.Bookmark.Button"); - } - - public getHeaderId(): string { - return Constants.Ids.bookmarkHeaderTitle; - } -} - -let component = PreviewViewerBookmarkHeaderClass.componentize(); -export {component as PreviewViewerBookmarkHeader}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerFullPageHeader.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerFullPageHeader.tsx deleted file mode 100644 index 9bfbeeee..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerFullPageHeader.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import {Constants} from "../../../constants"; -import {Localization} from "../../../localization/localization"; - -import {PreviewViewerTitleOnlyHeaderComponentBase} from "./previewViewerTitleOnlyHeaderComponentBase"; - -class PreviewViewerFullPageHeaderClass extends PreviewViewerTitleOnlyHeaderComponentBase { - public getControlGroupId(): string { - return Constants.Ids.fullPageControl; - } - - public getHeader(): string { - return Localization.getLocalizedString("WebClipper.ClipType.ScreenShot.Button"); - } - - public getHeaderId(): string { - return Constants.Ids.fullPageHeaderTitle; - } -} - -let component = PreviewViewerFullPageHeaderClass.componentize(); -export {component as PreviewViewerFullPageHeader}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerHeaderComponentBase.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerHeaderComponentBase.tsx deleted file mode 100644 index 1b86d40d..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerHeaderComponentBase.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import {ComponentBase} from "../../componentBase"; - -/** - * Shared class names used by header elements for consistent styling. - */ -export module HeaderClasses { - export module Button { - export let active = " active"; - export let controlButton = " control-button"; - export let relatedButtons = " related-buttons"; - export let activeControlButton = active + controlButton; - } -} - -export interface ControlGroup { - id?: string; - className?: string; - role?: string; - isAriaSet?: boolean; - innerElements: any[]; -} - -/** - * Represents a preview header that can contain multiple control button groups, i.e., groups - * of buttons or similar header entities. Child classes need to simply declare each control's - * buttons. - */ -export abstract class PreviewViewerHeaderComponentBase extends ComponentBase { - /** - * Gets the list of control groups to be rendered. - */ - abstract getControlGroups(): ControlGroup[]; - - render() { - let controlButtonGroup = "control-button-group"; - - let renderables = []; - let buttonGroups = this.getControlGroups(); - - for (let i = 0; i < buttonGroups.length; i++) { - let currentButtonGroup = buttonGroups[i]; - let id = currentButtonGroup.id; - let className = currentButtonGroup.className; - let role = currentButtonGroup.role; - let isAriaSet = currentButtonGroup.isAriaSet; - if (isAriaSet) { - let setSize = currentButtonGroup.innerElements.length; - for (let j = 0; j < setSize; j++) { - currentButtonGroup.innerElements[j].attrs["aria-posinset"] = j + 1; - currentButtonGroup.innerElements[j].attrs["aria-setsize"] = setSize; - } - - } - renderables.push( -
    - {currentButtonGroup.innerElements} -
    ); - } - - return ( -
    - {renderables} -
    - ); - } -} diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerPdfHeader.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerPdfHeader.tsx deleted file mode 100644 index 59dcac0d..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerPdfHeader.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import {Constants} from "../../../constants"; -import {Localization} from "../../../localization/localization"; - -import {PreviewViewerTitleOnlyHeaderComponentBase} from "./previewViewerTitleOnlyHeaderComponentBase"; - -class PreviewViewerPdfHeaderClass extends PreviewViewerTitleOnlyHeaderComponentBase { - public getControlGroupId(): string { - return Constants.Ids.pdfControl; - } - - public getHeader(): string { - return Localization.getLocalizedString("WebClipper.ClipType.Pdf.Button"); - } - - public getHeaderId(): string { - return Constants.Ids.pdfHeaderTitle; - } -} - -let component = PreviewViewerPdfHeaderClass.componentize(); -export {component as PreviewViewerPdfHeader}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerRegionHeader.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerRegionHeader.tsx deleted file mode 100644 index d0621ab3..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerRegionHeader.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import {Constants} from "../../../constants"; - -import {ExtensionUtils} from "../../../extensions/extensionUtils"; - -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp} from "../../clipperState"; -import {Status} from "../../status"; - -import {ControlGroup, HeaderClasses, PreviewViewerHeaderComponentBase} from "./previewViewerHeaderComponentBase"; - -class PreviewViewerRegionHeaderClass extends PreviewViewerHeaderComponentBase<{}, ClipperStateProp> { - addAnotherRegion() { - this.props.clipperState.setState({ - regionResult: { - status: Status.InProgress, - data: this.props.clipperState.regionResult.data - } - }); - } - - getControlGroups(): ControlGroup[] { - return [this.getScreenReaderTitleGroup(), this.getAddRegionGroup()]; - } - - private getScreenReaderTitleGroup() { - return { - className: Constants.Classes.srOnly, - innerElements: [ -
    {Localization.getLocalizedString("WebClipper.ClipType.Region.Button")}
    - ] - }; - } - - private getAddRegionGroup(): ControlGroup { - return { - id: Constants.Ids.addRegionControl, - innerElements: [ - - ] - }; - } -} - -let component = PreviewViewerRegionHeaderClass.componentize(); -export {component as PreviewViewerRegionHeader}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerRegionTitleOnlyHeader.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerRegionTitleOnlyHeader.tsx deleted file mode 100644 index bfb61427..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerRegionTitleOnlyHeader.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import {Constants} from "../../../constants"; -import {Localization} from "../../../localization/localization"; - -import {PreviewViewerTitleOnlyHeaderComponentBase} from "./previewViewerTitleOnlyHeaderComponentBase"; - -// Used by Edge as we need a title-only header for the Region mode used when the user takes an image selection -class PreviewViewerRegionTitleOnlyHeaderClass extends PreviewViewerTitleOnlyHeaderComponentBase { - public getControlGroupId(): string { - return Constants.Ids.regionControl; - } - - public getHeader(): string { - return Localization.getLocalizedString("WebClipper.ClipType.Region.Button"); - } - - public getHeaderId(): string { - return Constants.Ids.regionHeaderTitle; - } -} - -let component = PreviewViewerRegionTitleOnlyHeaderClass.componentize(); -export {component as PreviewViewerRegionTitleOnlyHeader}; diff --git a/src/scripts/clipperUI/components/previewViewer/previewViewerTitleOnlyHeaderComponentBase.tsx b/src/scripts/clipperUI/components/previewViewer/previewViewerTitleOnlyHeaderComponentBase.tsx deleted file mode 100644 index 2e187e8b..00000000 --- a/src/scripts/clipperUI/components/previewViewer/previewViewerTitleOnlyHeaderComponentBase.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp} from "../../clipperState"; - -import {ControlGroup, PreviewViewerHeaderComponentBase} from "./previewViewerHeaderComponentBase"; - -export abstract class PreviewViewerTitleOnlyHeaderComponentBase extends PreviewViewerHeaderComponentBase<{}, ClipperStateProp> { - public abstract getControlGroupId(): string; - public abstract getHeader(): string; - public abstract getHeaderId(): string; - - getControlGroups(): ControlGroup[] { - return [this.getTitleGroup(this.getControlGroupId(), this.getHeader(), this.getHeaderId())]; - } - - private getTitleGroup(controlGroupId: string, header: string, headerId: string): ControlGroup { - return { - id: controlGroupId, - innerElements: [ -
    - {header} -
    - ] - }; - } -} diff --git a/src/scripts/clipperUI/components/previewViewer/regionPreview.tsx b/src/scripts/clipperUI/components/previewViewer/regionPreview.tsx deleted file mode 100644 index a2974f1c..00000000 --- a/src/scripts/clipperUI/components/previewViewer/regionPreview.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import {Localization} from "../../../localization/localization"; - -import {ClipperStateProp, DataResult} from "../../clipperState"; -import {Status} from "../../status"; - -import {RegionSelection} from "../../components/regionSelection"; - -import {PreviewComponentBase} from "./previewComponentBase"; -import {PreviewViewerRegionHeader} from "./previewViewerRegionHeader"; -import {PreviewViewerRegionTitleOnlyHeader} from "./previewViewerRegionTitleOnlyHeader"; - -class RegionPreview extends PreviewComponentBase<{}, ClipperStateProp> { - protected getContentBodyForCurrentStatus(): any[] { - let state = this.props.clipperState; - return this.convertRegionResultToContentData(state.regionResult); - } - - protected getHeader() { - if (this.props.clipperState.injectOptions.enableRegionClipping) { - return ; - } - // This mode is made possible through image-selection clipping on supporting browsers - return ; - } - - protected getStatus(): Status { - return this.props.clipperState.regionResult.status; - } - - protected getTitleTextForCurrentStatus(): string { - let noContentFoundString = Localization.getLocalizedString("WebClipper.Preview.NoContentFound"); - - let previewStatus = this.getStatus(); - - switch (previewStatus) { - case Status.Succeeded: - return this.props.clipperState.previewGlobalInfo.previewTitleText; - case Status.NotStarted: - case Status.InProgress: - return Localization.getLocalizedString("WebClipper.Preview.LoadingMessage"); - default: - case Status.Failed: - return noContentFoundString; - } - } - - private convertRegionResultToContentData(result: DataResult): any[] { - let contentBody = []; - - switch (result.status) { - case Status.Succeeded: - let regions = this.props.clipperState.regionResult.data; - - // We want to disallow removal of regions in image-selection mode where the browser does not support the screenshot API - let onRemove = this.props.clipperState.injectOptions && this.props.clipperState.injectOptions.enableRegionClipping ? - (index: number) => { - let newRegions = this.props.clipperState.regionResult.data; - newRegions.splice(index, 1); - if (newRegions.length === 0) { - this.props.clipperState.setState({ regionResult: { status: Status.NotStarted, data: newRegions } }); - } else { - this.props.clipperState.setState({ regionResult: { status: Status.Succeeded, data: newRegions } }); - } - } : undefined; - - for (let i = 0; i < regions.length; i++) { - contentBody.push(); - } - break; - default: - case Status.NotStarted: - case Status.InProgress: - case Status.Failed: - break; - } - - return contentBody; - } -} - -let component = RegionPreview.componentize(); -export {component as RegionPreview}; diff --git a/src/scripts/clipperUI/components/previewViewer/selectionPreview.tsx b/src/scripts/clipperUI/components/previewViewer/selectionPreview.tsx deleted file mode 100644 index bf9dbc1d..00000000 --- a/src/scripts/clipperUI/components/previewViewer/selectionPreview.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import {ClipperStateProp} from "../../clipperState"; -import {Status} from "../../status"; - -import {EditorPreviewComponentBase, EditorPreviewState} from "./editorPreviewComponentBase"; - -class SelectionPreview extends EditorPreviewComponentBase { - protected getHighlightableContentBodyForCurrentStatus(): any { - return m.trust(this.props.clipperState.selectionPreviewInfo.previewBodyHtml); - } - - protected getStatus(): Status { - return Status.Succeeded; - } - - protected getTitleTextForCurrentStatus(): string { - return this.props.clipperState.previewGlobalInfo.previewTitleText; - } - - protected handleBodyChange(newBodyHtml: string) { - this.props.clipperState.setState({ - selectionPreviewInfo: { previewBodyHtml: newBodyHtml } - }); - } -} - -let component = SelectionPreview.componentize(); -export {component as SelectionPreview}; diff --git a/src/scripts/clipperUI/components/regionSelection.tsx b/src/scripts/clipperUI/components/regionSelection.tsx deleted file mode 100644 index 226b58fc..00000000 --- a/src/scripts/clipperUI/components/regionSelection.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as Log from "../../logging/log"; - -import {ExtensionUtils} from "../../extensions/extensionUtils"; - -import {ComponentBase} from "../componentBase"; -import { Localization } from "../../localization/localization"; - -export interface RegionSelectionProps { - imageSrc: string; - index: number; - onRemove?: (index: number) => void; -} - -class RegionSelectionClass extends ComponentBase<{}, RegionSelectionProps> { - buttonHandler() { - if (this.props.onRemove) { - this.props.onRemove(this.props.index); - } - } - - public getRemoveButton(): any[] { - // No remove button is rendered if there's no callback specified - return ( - this.props.onRemove - ? - {Localization.getLocalizedString("WebClipper.Preview.RemoveSelectedRegion")} - : undefined - ); - } - - public render() { - return ( -
    -

    - {this.getRemoveButton()} - {Localization.getLocalizedString("WebClipper.Preview.SelectedRegion")}/ -

    -
    - ); - } -} - -let component = RegionSelectionClass.componentize(); -export {component as RegionSelection}; diff --git a/src/scripts/clipperUI/components/rotatingMessageSpriteAnimation.tsx b/src/scripts/clipperUI/components/rotatingMessageSpriteAnimation.tsx deleted file mode 100644 index c8f44ec6..00000000 --- a/src/scripts/clipperUI/components/rotatingMessageSpriteAnimation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import {Constants} from "../../constants"; - -import {Localization} from "../../localization/localization"; - -import {ComponentBase} from "../componentBase"; - -import {SpriteAnimation, SpriteAnimationProps} from "./spriteAnimation"; - -interface RotatingMessageSpriteAnimationProps extends SpriteAnimationProps { - messageToDisplay?: string; - shouldDisplayMessage?: boolean; -} - -interface SpriteAnimationState { - inProgress: boolean; -} - -class RotatingMessageSpriteAnimationClass extends ComponentBase { - render() { - const shouldDisplayMessage = "shouldDisplayMessage" in this.props ? this.props.shouldDisplayMessage : true; - const message = shouldDisplayMessage ? (this.props.messageToDisplay ? this.props.messageToDisplay : Localization.getLocalizedString("WebClipper.Preview.Spinner.ClipAnyTimeInFullPage")) : undefined; - - return ( -
    - - {shouldDisplayMessage ? -
    - {message} -
    - : ""} -
    - ); - } -} - -let component = RotatingMessageSpriteAnimationClass.componentize(); -export {component as RotatingMessageSpriteAnimation}; diff --git a/src/scripts/clipperUI/components/sectionPicker.tsx b/src/scripts/clipperUI/components/sectionPicker.tsx deleted file mode 100644 index 2846b962..00000000 --- a/src/scripts/clipperUI/components/sectionPicker.tsx +++ /dev/null @@ -1,394 +0,0 @@ -/// -/// - -import {Constants} from "../../constants"; -import {Localization} from "../../localization/localization"; -import * as Log from "../../logging/log"; -import {Settings} from "../../settings"; -import {ClipperStorageKeys} from "../../storage/clipperStorageKeys"; -import {ClipperStateProp} from "../clipperState"; -import {ClipperStateUtilities} from "../clipperStateUtilities"; -import {ComponentBase} from "../componentBase"; -import {Clipper} from "../frontEndGlobals"; -import {OneNoteApiUtils} from "../oneNoteApiUtils"; -import { Status } from "../status"; - -export interface SectionPickerState { - notebooks?: OneNoteApi.Notebook[]; - status?: Status; - apiResponseCode?: string; - curSection?: { - path: string; - section: OneNoteApi.Section; - }; -} - -interface SectionPickerProp extends ClipperStateProp { - onPopupToggle: (shouldNowBeOpen: boolean) => void; -} - -export class SectionPickerClass extends ComponentBase { - static dataSource: OneNotePicker.OneNotePickerDataSource; - - getInitialState(): SectionPickerState { - return { - notebooks: undefined, - status: Status.NotStarted, - curSection: undefined - }; - } - - onSectionClicked(curSection: any) { - this.props.clipperState.setState({ - saveLocation: curSection.section.id - }); - this.setState({ - curSection: curSection - }); - Clipper.storeValue(ClipperStorageKeys.currentSelectedSection, JSON.stringify(curSection)); - Clipper.logger.logClickEvent(Log.Click.Label.sectionComponent); - } - - onPopupToggle(shouldNowBeOpen: boolean) { - if (shouldNowBeOpen) { - // If the user selects a section, onPopupToggle will fire because it closes the popup, even though it wasn't a click - // so logging only when they open it is potentially the next best thing - Clipper.logger.logClickEvent(Log.Click.Label.sectionPickerLocationContainer); - } - this.props.onPopupToggle(shouldNowBeOpen); - if (shouldNowBeOpen) { - // Focus the selected section and attach arrow key navigation for keyboard users - setTimeout(() => { - let sectionPickerPopup = document.getElementById("sectionPickerContainer"); - - let curSectionId = this.state.curSection && this.state.curSection.section ? this.state.curSection.section.id : undefined; - let elementToFocus: HTMLElement; - if (curSectionId) { - elementToFocus = document.getElementById(curSectionId) as HTMLElement; - } - if (!elementToFocus && sectionPickerPopup) { - // Fall back to the first keyboard-navigable item in the section picker popup - elementToFocus = sectionPickerPopup.querySelector("[tabindex]:not([tabindex=\"-1\"])") as HTMLElement; - } - if (elementToFocus) { - elementToFocus.focus(); - } - - // Attach Up/Down arrow key navigation (OneNotePicker only handles Enter/Tab) - if (sectionPickerPopup && !sectionPickerPopup.getAttribute("data-arrow-key-handler-attached")) { - sectionPickerPopup.setAttribute("data-arrow-key-handler-attached", "true"); - sectionPickerPopup.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.which !== Constants.KeyCodes.up && e.which !== Constants.KeyCodes.down) { - return; - } - e.preventDefault(); - let focusableItems = Array.prototype.slice.call( - sectionPickerPopup.querySelectorAll("[tabindex]:not([tabindex=\"-1\"])") - ).filter((el) => { - let parent = (el as HTMLElement).parentElement; - return !parent || !parent.closest(".Closed"); - }) as HTMLElement[]; - if (focusableItems.length === 0) { - return; - } - let currentIndex = focusableItems.indexOf(document.activeElement as HTMLElement); - if (e.which === Constants.KeyCodes.up) { - let prevIndex = currentIndex <= 0 ? 0 : currentIndex - 1; - focusableItems[prevIndex].focus(); - } else { - let nextIndex = currentIndex >= focusableItems.length - 1 ? focusableItems.length - 1 : currentIndex + 1; - focusableItems[nextIndex].focus(); - } - }, true); - } - }, 0); - } - } - - // Returns true if successful; false otherwise - setDataSource(): boolean { - if (!ClipperStateUtilities.isUserLoggedIn(this.props.clipperState)) { - return false; - } - - let userToken = this.props.clipperState.userResult.data.user.accessToken; - SectionPickerClass.dataSource = new OneNotePicker.OneNotePickerDataSource(userToken); - return true; - } - - // Begins by updating state with information found in local storage, then retrieves and stores fresh notebook information - // from the API. If the user does not have a previous section selection in storage, or has not made a section selection yet, - // additionally set the current section to the default section. - retrieveAndUpdateNotebookAndSectionSelection(): Promise { - return new Promise((resolve, reject) => { - if (this.dataSourceUninitialized()) { - this.setDataSource(); - } - - this.setState({ - status: Status.InProgress - }); - - // Always set the values with what is in local storage, and when the XHR returns it will overwrite if necessary - this.fetchCachedNotebookAndSectionInfoAsState((cachedInfoAsState: SectionPickerState) => { - if (cachedInfoAsState) { - this.setState(cachedInfoAsState); - this.props.clipperState.setState({ - saveLocation: cachedInfoAsState.curSection && cachedInfoAsState.curSection.section ? cachedInfoAsState.curSection.section.id : "" - }); - } - - let getNotebooksEvent: Log.Event.PromiseEvent = new Log.Event.PromiseEvent(Log.Event.Label.GetNotebooks); - - this.fetchFreshNotebooks(Clipper.getUserSessionId()).then((responsePackage) => { - let correlationId = responsePackage.request.getResponseHeader(Constants.HeaderValues.correlationId); - getNotebooksEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, correlationId); - - let freshNotebooks = responsePackage.parsedResponse; - if (!freshNotebooks) { - getNotebooksEvent.setStatus(Log.Status.Failed); - let error = {error: "GetNotebooks Promise was resolved but returned null or undefined value for notebooks."}; - getNotebooksEvent.setFailureInfo(error); - this.setState({ - status: Status.Failed - }); - reject(error); - return; - } - - getNotebooksEvent.setCustomProperty(Log.PropertyName.Custom.MaxDepth, OneNoteApi.NotebookUtils.getDepthOfNotebooks(freshNotebooks)); - - Clipper.storeValue(ClipperStorageKeys.cachedNotebooks, JSON.stringify(freshNotebooks)); - - // The curSection property is the default section found in the notebook list - let freshNotebooksAsState = SectionPickerClass.convertNotebookListToState(freshNotebooks); - - let shouldOverrideCurSectionWithDefault = true; - if (this.state.curSection && this.state.curSection.section) { - // The user has already selected a section ... - let currentSectionStillExists = OneNoteApi.NotebookUtils.sectionExistsInNotebooks(freshNotebooks, this.state.curSection.section.id); - if (currentSectionStillExists) { - // ... which exists, so we don't override it with the default - freshNotebooksAsState.curSection = this.state.curSection; - shouldOverrideCurSectionWithDefault = false; - } - getNotebooksEvent.setCustomProperty(Log.PropertyName.Custom.CurrentSectionStillExists, currentSectionStillExists); - } - - if (shouldOverrideCurSectionWithDefault) { - // A default section was found, so we set it as currently selected since the user has not made a valid selection yet - // curSection can be undefined if there's no default found, which is fine - Clipper.storeValue(ClipperStorageKeys.currentSelectedSection, JSON.stringify(freshNotebooksAsState.curSection)); - this.props.clipperState.setState({saveLocation: freshNotebooksAsState.curSection ? freshNotebooksAsState.curSection.section.id : undefined}); - } - - this.setState(freshNotebooksAsState); - resolve(freshNotebooksAsState); - }).catch((failure: OneNoteApi.RequestError) => { - this.setState({ - apiResponseCode: OneNoteApiUtils.getApiResponseCode(failure) - }); - - if (this.state.notebooks) { - failure.error += ". Falling back to storage."; - } else { - this.setState({ - status: Status.Failed - }); - } - OneNoteApiUtils.logOneNoteApiRequestError(getNotebooksEvent, failure); - reject(failure); - }).then(() => { - Clipper.logger.logEvent(getNotebooksEvent); - }); - }); - }); - } - - dataSourceUninitialized(): boolean { - return !SectionPickerClass.dataSource || - !SectionPickerClass.dataSource.authToken || - !ClipperStateUtilities.isUserLoggedIn(this.props.clipperState) || - (SectionPickerClass.dataSource.authToken !== this.props.clipperState.userResult.data.user.accessToken); - } - - // Retrieves the cached notebook list and last selected section from local storage in state form - fetchCachedNotebookAndSectionInfoAsState(callback: (state: SectionPickerState) => void): void { - Clipper.getStoredValue(ClipperStorageKeys.cachedNotebooks, (notebooks) => { - Clipper.getStoredValue(ClipperStorageKeys.currentSelectedSection, (curSection) => { - if (notebooks) { - let parsedNotebooks: any; - try { - parsedNotebooks = JSON.parse(notebooks); - } catch (e) { - Clipper.logger.logJsonParseUnexpected(notebooks); - } - - // It is ok for parsed value to be set to undefined, as this corresponds to the default section - let parsedCurSection: any; - if (parsedNotebooks && curSection) { - try { - parsedCurSection = JSON.parse(curSection); - } catch (e) { - Clipper.logger.logJsonParseUnexpected(curSection); - } - } - - callback({ - notebooks: parsedNotebooks, - status: Status.Succeeded, - curSection: parsedCurSection - }); - } else { - // Cached information not found in storage - callback(undefined); - } - }); - }); - } - - // Fetches the user's notebooks from OneNote API, returning both the notebook list and the XHR - fetchFreshNotebooks(sessionId: string): Promise> { - if (this.dataSourceUninitialized()) { - this.setDataSource(); - } - - let headers: { [key: string]: string } = {}; - headers[Constants.HeaderValues.appIdKey] = Settings.getSetting("App_Id"); - headers[Constants.HeaderValues.userSessionIdKey] = sessionId; - - return SectionPickerClass.dataSource.getNotebooks(headers); - } - - // Given a notebook list, converts it to state form where the curSection is the default section (or undefined if not found) - static convertNotebookListToState(notebooks: OneNoteApi.Notebook[]): SectionPickerState { - let pathToDefaultSection = OneNoteApi.NotebookUtils.getPathFromNotebooksToSection(notebooks, s => s.isDefault); - let defaultSectionInfo = SectionPickerClass.formatSectionInfoForStorage(pathToDefaultSection); - - return { - notebooks: notebooks, - status: Status.Succeeded, - curSection: defaultSectionInfo - }; - } - - static formatSectionInfoForStorage(pathToSection: OneNoteApi.SectionPathElement[]): { path: string, section: OneNoteApi.Section } { - if (!pathToSection || pathToSection.length === 0) { - return undefined; - } - return { - path: pathToSection.map(elem => elem.name).join(" > "), - section: pathToSection[pathToSection.length - 1] as OneNoteApi.Section - }; - } - - // Attach escape key handler to return focus to the dropdown button when Escape is pressed - attachEscapeFocusHandler(element: HTMLElement, isInitialized: boolean) { - if (!isInitialized) { - const escKeyCode = 27; - const handleKeyDown = (ev: KeyboardEvent) => { - if (ev.keyCode === escKeyCode) { - // Check if the dropdown popup is currently visible - let sectionPickerPopup = document.querySelector(".SectionPickerPopup"); - if (sectionPickerPopup) { - // The popup is open - schedule focus return after it closes - setTimeout(() => { - let locationButton = document.getElementById(Constants.Ids.sectionLocationContainer); - if (locationButton) { - locationButton.focus(); - } - }, 10); - } - } - }; - // Use capture phase to run before OneNotePicker's handler - document.addEventListener("keydown", handleKeyDown, true); - } - } - - addSrOnlyLocationDiv(element: HTMLElement) { - const pickerLinkElement = document.getElementById(Constants.Ids.sectionLocationContainer); - if (!pickerLinkElement) { - Clipper.logger.logTrace(Log.Trace.Label.General, Log.Trace.Level.Warning, `Unable to add sr-only div: Parent element with id ${Constants.Ids.sectionLocationContainer} not found`); - return; - } - const srDiv = document.createElement("div"); - srDiv.textContent = Localization.getLocalizedString("WebClipper.Label.ClipLocation") + ": "; - srDiv.setAttribute("class", Constants.Classes.srOnly); - // Make srDiv the first child of pickerLinkElement - pickerLinkElement.insertBefore(srDiv, pickerLinkElement.firstChild); - - // Attach escape key handler to return focus to the dropdown button - this.attachEscapeFocusHandler(element, false); - } - - render() { - if (this.dataSourceUninitialized()) { - // This logic gets executed on app launch (if already signed in) and whenever the user signs in or out ... - let dataSourceSet = this.setDataSource(); - if (dataSourceSet) { - // ... so we want to ensure we only fetch fresh notebooks on the sign in - this.retrieveAndUpdateNotebookAndSectionSelection(); - } - } else if (!this.state.notebooks && this.state.status === Status.NotStarted) { - // Since we re-render this with initial state when we switch between modes that do or do not render this component, we don't want to lose our - // stored notebooks in state - this.fetchCachedNotebookAndSectionInfoAsState((cachedInfoAsState: SectionPickerState) => { - if (cachedInfoAsState) { - this.setState(cachedInfoAsState); - this.props.clipperState.setState({ - saveLocation: cachedInfoAsState.curSection && cachedInfoAsState.curSection.section ? cachedInfoAsState.curSection.section.id : "" - }); - } - }); - } - - let localizedStrings = { - defaultLocation: Localization.getLocalizedString("WebClipper.SectionPicker.DefaultLocation"), - loadingNotebooks: Localization.getLocalizedString("WebClipper.SectionPicker.LoadingNotebooks"), - noNotebooksFound: Localization.getLocalizedString("WebClipper.SectionPicker.NoNotebooksFound"), - notebookLoadFailureMessage: Localization.getLocalizedString("WebClipper.SectionPicker.NotebookLoadFailureMessage") - }; - - // Compute all the necessary properties for the Picker based on the state we are in - let curSectionId: string, textToDisplay: string = localizedStrings.defaultLocation; - - // We could have returned correctly, but there is no default Notebook - if (this.state.status === Status.Succeeded) { - if (this.state.curSection) { - curSectionId = this.state.curSection.section.id; - textToDisplay = this.state.curSection.path; - } - } - - // If we can show a better message, especially an actionable one, we do - if (this.state.apiResponseCode && !OneNoteApiUtils.isRetryable(this.state.apiResponseCode)) { - localizedStrings.notebookLoadFailureMessage = OneNoteApiUtils.getLocalizedErrorMessageForGetNotebooks(this.state.apiResponseCode); - } - - let locationString = Localization.getLocalizedString("WebClipper.Label.ClipLocation"); - - return ( -
    -
    - - {locationString} - -
    - -
    - ); - } -} - -let component = SectionPickerClass.componentize(); -export {component as SectionPicker}; diff --git a/src/scripts/clipperUI/components/spriteAnimation.tsx b/src/scripts/clipperUI/components/spriteAnimation.tsx deleted file mode 100644 index 5e7c543b..00000000 --- a/src/scripts/clipperUI/components/spriteAnimation.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import {ComponentBase} from "../componentBase"; -import {Constants} from "../../constants"; -import {Localization} from "../../localization/localization"; - -/** - * Sequences images into an animation from a vertical sprite sheet - */ - -export interface SpriteAnimationProps { - shouldTakeFocus?: boolean; - spriteUrl: string; - imageHeight: number; - imageWidth?: number; - totalFrameCount: number; - loop?: boolean; - ariaLabel?: string; - tabIndex?: number; -} - -export interface SpriteAnimationState { - inProgress: boolean; -} - -class SpriteAnimationClass extends ComponentBase { - private currentFrame = 0; - private currentTime = this.rightNow(); - private spinnerElement: HTMLDivElement; - - initiallySetFocus(element: HTMLElement) { - if (this.props.shouldTakeFocus) { - element.focus(); - } - } - - getInitialState(): SpriteAnimationState { - requestAnimationFrame(() => { - this.updateFrame(); - }); - - return { - inProgress: true - }; - } - - updateFrame() { - if (!this.spinnerElement) { - this.stop(); - return; - } - - if (!this.state.inProgress) { - return; - } - - let time = this.rightNow(); - let fps = 24; - let delta = (time - this.currentTime) / 1000; - this.currentTime = time; - - this.currentFrame += (delta * fps); - let frameNumber = Math.floor(this.currentFrame); - if (frameNumber >= this.props.totalFrameCount) { - this.onFinishLoop(); - - if (this.props.loop) { - frameNumber = this.currentFrame = 0; - } else { - this.stop(); - frameNumber = this.currentFrame = this.props.totalFrameCount - 1; - } - } - - let backgroundPosition = (frameNumber * this.props.imageHeight); - this.spinnerElement.style.backgroundPosition = "0 -" + backgroundPosition + "px"; - requestAnimationFrame(() => { - this.updateFrame(); - }); - } - - stop() { - this.setState({ inProgress: false }); - } - - /** - * Fires when the animation finishes its loop - * Note: Intended to be overwritten as an event handler - */ - onFinishLoop() { - ////console.log("onFinishLoop"); - } - - /** - * If window.performance exists, then use window.performance.now since it is faster, - * else use Date.now() - */ - rightNow(): number { - if (window.performance && window.performance.now) { - return window.performance.now(); - } else { - return Date.now(); - } - } - - configForSpinner(element: HTMLDivElement, isInitialized: boolean, context: any) { - this.spinnerElement = element; - if (!isInitialized) { - this.initiallySetFocus(element); - } - context.onunload = () => { - this.stop(); - }; - } - - render() { - let imageWidth = this.props.imageWidth ? this.props.imageWidth : 32; - - let ariaLabel = this.props.ariaLabel ? this.props.ariaLabel : Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.Loading"); - - let tabIndex = this.props.tabIndex ? this.props.tabIndex : 290; - - let style = { - backgroundImage: "url(" + this.props.spriteUrl + ")", - backgroundRepeat: "no-repeat", - height: this.props.imageHeight + "px", - width: imageWidth + "px" - }; - - return ( -
    - ); - } -} - -let component = SpriteAnimationClass.componentize(); -export {component as SpriteAnimation}; diff --git a/src/scripts/clipperUI/localeSpecificTasks.ts b/src/scripts/clipperUI/localeSpecificTasks.ts deleted file mode 100644 index a5ba6870..00000000 --- a/src/scripts/clipperUI/localeSpecificTasks.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {Rtl} from "../localization/rtl"; - -import {ClipperStorageKeys} from "../storage/clipperStorageKeys"; - -/* - * Responsible for executing locale-specific tasks before initializing and displaying - * the Clipper. - */ -export class LocaleSpecificTasks { - public static execute(locale: string): void { - this.appendDirectionalCssToHead(locale); - } - - /* - * Appends either the LTR or RTL css, whichever is suitable for the given locale - */ - private static appendDirectionalCssToHead(locale: string): void { - let filenamePostfix = Rtl.isRtl(locale) ? "-rtl.css" : ".css"; - let cssFileNames = ["clipper", "sectionPicker"]; - for (let i = 0; i < cssFileNames.length; i++) { - let clipperCssFilename = cssFileNames[i] + filenamePostfix; - let clipperCssElem = document.createElement("link"); - clipperCssElem.setAttribute("rel", "stylesheet"); - clipperCssElem.setAttribute("type", "text/css"); - clipperCssElem.setAttribute("href", clipperCssFilename); - document.getElementsByTagName("head")[0].appendChild(clipperCssElem); - } - } -} - -let localeOverride: string; -try { - localeOverride = window.localStorage.getItem(ClipperStorageKeys.displayLanguageOverride); -} catch (e) { } - -// navigator.userLanguage is only available in IE, and Typescript will not recognize this property -LocaleSpecificTasks.execute(localeOverride || navigator.language || (navigator).userLanguage); diff --git a/src/scripts/clipperUI/mainController.tsx b/src/scripts/clipperUI/mainController.tsx deleted file mode 100644 index 7301fa81..00000000 --- a/src/scripts/clipperUI/mainController.tsx +++ /dev/null @@ -1,448 +0,0 @@ -declare var Velocity: jquery.velocity.VelocityStatic; - -import {SmartValue} from "../communicator/smartValue"; -import {Constants} from "../constants"; -import {Localization} from "../localization/localization"; -import * as Log from "../logging/log"; -import {AnimationHelper} from "./animations/animationHelper"; -import {AnimationState} from "./animations/animationState"; -import {AnimationStrategy} from "./animations/animationStrategy"; -import {ExpandFromRightAnimationStrategy} from "./animations/expandFromRightAnimationStrategy"; -import {FadeInAnimationStrategy} from "./animations/fadeInAnimationStrategy"; -import {SlidingHeightAnimationStrategy} from "./animations/slidingHeightAnimationStrategy"; -import {ClipMode} from "./clipMode"; -import {ClipperStateProp} from "./clipperState"; -import {ClipperStateUtilities} from "./clipperStateUtilities"; -import {ComponentBase} from "./componentBase"; -import {CloseButton} from "./components/closeButton"; -import {Footer} from "./components/footer"; -import {Clipper} from "./frontEndGlobals"; -import {OneNoteApiUtils} from "./oneNoteApiUtils"; -import {ClippingPanel} from "./panels/clippingPanel"; -import {ClippingPanelWithDelayedMessage} from "./panels/clippingPanelWithDelayedMessage"; -import {ClippingPanelWithProgressIndicator} from "./panels/clippingPanelWithProgressIndicator"; -import {DialogButton} from "./panels/dialogPanel"; -import {ErrorDialogPanel} from "./panels/errorDialogPanel"; -import {LoadingPanel} from "./panels/loadingPanel"; -import {OptionsPanel} from "./panels/optionsPanel"; -import {RatingsPanel} from "./panels/ratingsPanel"; -import {RegionSelectingPanel} from "./panels/regionSelectingPanel"; -import {SignInPanel} from "./panels/signInPanel"; -import {SuccessPanel} from "./panels/successPanel"; -import {Status} from "./status"; - -export enum CloseReason { - CloseButton, - EscPress -} - -export enum PanelType { - None, - BadState, - Loading, - SignInNeeded, - ClipOptions, - RegionInstructions, - ClippingToApi, - ClippingFailure, - ClippingSuccess -} - -export interface MainControllerState { - currentPanel?: PanelType; - ratingsPanelAnimationState?: SmartValue; // stored for when the ratings subpanel re-renders while the MainController does not -} - -export interface MainControllerProps extends ClipperStateProp { - onSignInInvoked: () => void; - onSignOutInvoked: (authType: string) => void; - updateFrameHeight: (newContainerHeight: number) => void; - onStartClip: () => void; - clearKeepAlive: () => void; -} - -export class MainControllerClass extends ComponentBase { - private popupIsOpen: boolean; - - private controllerAnimationStrategy: AnimationStrategy; - private panelAnimationStrategy: AnimationStrategy; - private heightAnimationStrategy: AnimationStrategy; - - constructor(props: MainControllerProps) { - super(props); - - this.initAnimationStrategy(); - - // The user could have pressed esc when Clipper iframe was not in focus - Clipper.getInjectCommunicator().registerFunction(Constants.FunctionKeys.escHandler, () => { - this.handleEscPress(); - }); - - document.onkeydown = (event) => { - if (event.keyCode === Constants.KeyCodes.esc) { - this.handleEscPress(); - } - // Handle focus trap for success panel (A11y fix) - if (event.keyCode === Constants.KeyCodes.tab) { - this.handleFocusTrap(event); - } - }; - } - - getInitialState(): MainControllerState { - return { - currentPanel: PanelType.None - }; - } - - handleEscPress() { - if (this.isCloseable()) { - this.closeClipper(CloseReason.EscPress); - } - } - - // Prevents Tab key from moving focus outside the popup - handleFocusTrap(event: KeyboardEvent) { - // Only trap focus for specific panels that need it - if (this.state.currentPanel !== PanelType.ClippingSuccess && - this.state.currentPanel !== PanelType.RegionInstructions) { - return; - } - - let mainController = document.getElementById(Constants.Ids.mainController); - if (!mainController) { - return; - } - - // Get all focusable elements within the main controller - let focusableElements = mainController.querySelectorAll( - "a[tabindex], button[tabindex], input[tabindex], [tabindex]:not([tabindex='-1'])" - ); - - if (focusableElements.length === 0) { - return; - } - - // Filter to only include elements with positive tabIndex and sort by tabIndex - let sortedFocusables: HTMLElement[] = []; - for (let i = 0; i < focusableElements.length; i++) { - let element = focusableElements[i] as HTMLElement; - if (element.tabIndex >= 0) { - sortedFocusables.push(element); - } - } - - if (sortedFocusables.length === 0) { - return; - } - - // Sort by tabIndex - sortedFocusables.sort((a, b) => a.tabIndex - b.tabIndex); - - // Always handle Tab manually to prevent focus from escaping the iframe - event.preventDefault(); - - // Find current element's index - let currentIndex = -1; - for (let i = 0; i < sortedFocusables.length; i++) { - if (document.activeElement === sortedFocusables[i]) { - currentIndex = i; - break; - } - } - - let nextIndex: number; - if (event.shiftKey) { - // Shift + Tab: move to previous, wrap to last if at first - nextIndex = currentIndex <= 0 ? sortedFocusables.length - 1 : currentIndex - 1; - } else { - // Tab: move to next, wrap to first if at last - nextIndex = currentIndex >= sortedFocusables.length - 1 ? 0 : currentIndex + 1; - } - - sortedFocusables[nextIndex].focus(); - } - - initAnimationStrategy() { - this.controllerAnimationStrategy = new ExpandFromRightAnimationStrategy({ - extShouldAnimateIn: () => { return this.props.clipperState.uiExpanded; }, - extShouldAnimateOut: () => { return !this.props.clipperState.uiExpanded; }, - onBeforeAnimateOut: () => { this.setState({currentPanel: PanelType.None}); }, - onBeforeAnimateIn: () => { this.props.clipperState.reset(); }, - onAnimateInExpand: () => { this.setState({currentPanel: this.getPanelTypeToShow()}); }, - onAfterAnimateOut: () => { Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.hideUi); } - }); - - this.panelAnimationStrategy = new FadeInAnimationStrategy({ - extShouldAnimateIn: () => { return this.state.currentPanel !== PanelType.None; }, - extShouldAnimateOut: () => { return this.getPanelTypeToShow() !== this.state.currentPanel; }, - onAfterAnimateOut: () => { this.setState({currentPanel: this.getPanelTypeToShow()}); }, - onAfterAnimateIn: () => { this.setState({currentPanel: this.getPanelTypeToShow()}); } - }); - - this.heightAnimationStrategy = new SlidingHeightAnimationStrategy(Constants.Ids.mainController, { - onAfterHeightAnimatorDraw: (newHeightInfo) => { - if (this.popupIsOpen) { - // Sometimes during the delay, we open the popup, so the frame height update needs to account for that too - this.updateFrameHeightAfterPopupToggle(this.popupIsOpen); - } else { - this.props.updateFrameHeight(newHeightInfo.newContainerHeight); - } - } - }); - } - - isCloseable() { - let panelType = this.state.currentPanel; - return panelType !== PanelType.None && panelType !== PanelType.ClippingToApi; - } - - onPopupToggle(shouldNowBeOpen: boolean) { - this.popupIsOpen = shouldNowBeOpen; - this.updateFrameHeightAfterPopupToggle(shouldNowBeOpen); - } - - private updateFrameHeightAfterPopupToggle(shouldNowBeOpen: boolean) { - let saveToLocationContainer = document.getElementById(Constants.Ids.saveToLocationContainer); - if (saveToLocationContainer) { - let currentLocationContainerPosition: ClientRect = saveToLocationContainer.getBoundingClientRect(); - let aboutToOpenHeight = Constants.Styles.sectionPickerContainerHeight + currentLocationContainerPosition.top + currentLocationContainerPosition.height; - let aboutToCloseHeight = document.getElementById(Constants.Ids.mainController).offsetHeight; - let newHeight = shouldNowBeOpen ? aboutToOpenHeight : aboutToCloseHeight; - this.props.updateFrameHeight(newHeight); - } - } - - getPanelTypeToShow(): PanelType { - if (this.props.clipperState.badState) { - return PanelType.BadState; - } - - if (!this.props.clipperState.uiExpanded) { - return PanelType.None; - } - - if ((this.props.clipperState.userResult && this.props.clipperState.userResult.status === Status.InProgress) || - this.props.clipperState.fetchLocStringStatus === Status.InProgress || !this.props.clipperState.invokeOptions) { - return PanelType.Loading; - } - - if (!ClipperStateUtilities.isUserLoggedIn(this.props.clipperState, true)) { - return PanelType.SignInNeeded; - } - - if (this.props.clipperState.currentMode.get() === ClipMode.Region && this.props.clipperState.regionResult.status !== Status.Succeeded) { - switch (this.props.clipperState.regionResult.status) { - case Status.InProgress: - return PanelType.Loading; - default: - return PanelType.RegionInstructions; - } - } - - switch (this.props.clipperState.oneNoteApiResult.status) { - default: - case Status.NotStarted: - return PanelType.ClipOptions; - case Status.InProgress: - return PanelType.ClippingToApi; - case Status.Failed: - return PanelType.ClippingFailure; - case Status.Succeeded: - return PanelType.ClippingSuccess; - } - } - - closeClipper(closeReason?: CloseReason) { - let closeEvent = new Log.Event.BaseEvent(Log.Event.Label.CloseClipper); - closeEvent.setCustomProperty(Log.PropertyName.Custom.CurrentPanel, PanelType[this.state.currentPanel]); - closeEvent.setCustomProperty(Log.PropertyName.Custom.CloseReason, CloseReason[closeReason]); - Clipper.logger.logEvent(closeEvent); - - this.props.clearKeepAlive(); - - // Clear region selections on clipper exit rather than invoke to avoid conflicting logic with scenarios like context menu image selection - this.props.clipperState.setState({ - uiExpanded: false, - regionResult: {status: Status.NotStarted, data: []} - }); - } - - onMainControllerDraw(mainControllerElement: HTMLElement) { - this.controllerAnimationStrategy.animate(mainControllerElement); - } - - onPanelAnimatorDraw(panelAnimator: HTMLElement) { - let panelTypeToShow: PanelType = this.getPanelTypeToShow(); - let panelIsCorrect = panelTypeToShow === this.state.currentPanel; - - if (panelTypeToShow === PanelType.ClipOptions && this.state.currentPanel !== PanelType.ClipOptions) { - this.popupIsOpen = false; - } - - this.panelAnimationStrategy.animate(panelAnimator); - - if (!panelIsCorrect && this.panelAnimationStrategy.getAnimationState() === AnimationState.GoingIn) { - // We'll speed things up by stopping the current one, and trigger the next one - AnimationHelper.stopAnimationsThen(panelAnimator, () => { - this.panelAnimationStrategy.setAnimationState(AnimationState.In); - this.setState({}); - }); - } - } - - onHeightAnimatorDraw(heightAnimator: HTMLElement) { - this.heightAnimationStrategy.animate(heightAnimator); - } - - getCurrentPanel() { - let panelType = this.state.currentPanel; - let buttons: DialogButton[] = []; - switch (panelType) { - case PanelType.BadState: - buttons.push({ - id: Constants.Ids.refreshPageButton, - label: Localization.getLocalizedString("WebClipper.Action.RefreshPage"), - handler: () => { - Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.refreshPage); - } - }); - return ; - case PanelType.Loading: - return ; - case PanelType.SignInNeeded: - return ; - case PanelType.ClipOptions: - return ; - case PanelType.RegionInstructions: - return ; - case PanelType.ClippingToApi: - if (this.props.clipperState.currentMode.get() === ClipMode.Pdf) { - if (this.props.clipperState.pdfPreviewInfo.shouldDistributePages) { - return ; - } - - return ; - } - return ; - case PanelType.ClippingFailure: - let error = this.props.clipperState.oneNoteApiResult.data as OneNoteApi.RequestError; - let apiResponseCode: string = OneNoteApiUtils.getApiResponseCode(error); - - if (OneNoteApiUtils.isRetryable(apiResponseCode)) { - buttons.push({ - id: Constants.Ids.dialogTryAgainButton, - label: Localization.getLocalizedString("WebClipper.Action.TryAgain"), - handler: this.props.onStartClip - }); - } - if (OneNoteApiUtils.requiresSignout(apiResponseCode)) { - buttons.push({ - id: Constants.Ids.dialogSignOutButton, - label: Localization.getLocalizedString("WebClipper.Action.SignOut"), - handler: () => { - if (this.props.onSignOutInvoked) { - this.props.onSignOutInvoked(this.props.clipperState.userResult.data.user.authType); - } - } - }); - } else { - buttons.push({ - id: Constants.Ids.dialogBackButton, - label: Localization.getLocalizedString("WebClipper.Action.BackToHome"), - handler: () => { - this.props.clipperState.setState({ - oneNoteApiResult: { - data: this.props.clipperState.oneNoteApiResult.data, - status: Status.NotStarted - } - }); - } - }); - } - - return ; - case PanelType.ClippingSuccess: - let panels: any[] = []; - - if (!this.state.ratingsPanelAnimationState) { - this.state.ratingsPanelAnimationState = new SmartValue(AnimationState.Out); - } - - if (this.props.clipperState.showRatingsPrompt) { - panels.push(); - } - - return panels; - default: - return undefined; - } - } - - getCloseButtonForType() { - if (this.isCloseable()) { - return ( - - ); - } else { - return undefined; - } - } - - getCurrentFooter(): any { - let panelType = this.state.currentPanel; - switch (panelType) { - case PanelType.ClipOptions: - case PanelType.ClippingFailure: - let error = this.props.clipperState.oneNoteApiResult.data as OneNoteApi.RequestError; - let apiResponseCode: string = OneNoteApiUtils.getApiResponseCode(error); - if (OneNoteApiUtils.requiresSignout(apiResponseCode)) { - // If the ResponseCode requires a SignOut to fix, then the dialogButton will handle SignOut - // so we will not show the footer. If it doesn't require signout, show the Footer - return undefined; - } - /* falls through */ - case PanelType.SignInNeeded: - return
    ; - case PanelType.ClippingSuccess: - /* falls through */ - default: - return undefined; - } - } - - render() { - let panelToRender = this.getCurrentPanel(); - let closeButtonToRender = this.getCloseButtonForType(); - let footerToRender = this.getCurrentFooter(); - - return ( -
    - {closeButtonToRender} -
    -
    -
    - {panelToRender} - {footerToRender} -
    -
    -
    -
    - ); - } -} - -let component = MainControllerClass.componentize(); -export {component as MainController}; diff --git a/src/scripts/clipperUI/oneNoteApiUtils.ts b/src/scripts/clipperUI/oneNoteApiUtils.ts deleted file mode 100644 index 26981b70..00000000 --- a/src/scripts/clipperUI/oneNoteApiUtils.ts +++ /dev/null @@ -1,147 +0,0 @@ -import {Constants} from "../constants"; - -import {Localization} from "../localization/localization"; - -import * as Log from "../logging/log"; - -import {Clipper} from "./frontEndGlobals"; - -export module OneNoteApiUtils { - export module Limits { - export var imagesPerRequestLimit = 30; - } - - export function logOneNoteApiRequestError(event: Log.Event.PromiseEvent, error: OneNoteApi.RequestError) { - if (!event || !error) { - return; - } - - event.setCustomProperty(Log.PropertyName.Custom.CorrelationId, error.responseHeaders[Constants.HeaderValues.correlationId]); - event.setStatus(Log.Status.Failed); - event.setFailureInfo(error); - - let apiResponseCode: string = getApiResponseCode(error); - if (!apiResponseCode) { - return; - } - - let responseCodeInfo = getResponseCodeInformation(apiResponseCode); - if (!responseCodeInfo) { - Clipper.logger.logFailure(Log.Failure.Label.UnhandledApiCode, Log.Failure.Type.Unexpected, - undefined, apiResponseCode); - } - - if (OneNoteApiUtils.isExpected(apiResponseCode)) { - event.setFailureType(Log.Failure.Type.Expected); - } - - event.setCustomProperty(Log.PropertyName.Custom.IsRetryable, OneNoteApiUtils.isRetryable(apiResponseCode)); - } - - export function getApiResponseCode(error: OneNoteApi.RequestError): string { - if (!error || !error.response) { - return; - } - - let responseAsJson: any; - try { - responseAsJson = JSON.parse(error.response); - } catch (e) { - Clipper.logger.logJsonParseUnexpected(error.response); - } - - let apiResponseCode: string; - if (responseAsJson && responseAsJson.error && responseAsJson.error.code) { - apiResponseCode = responseAsJson.error.code; - } - - return apiResponseCode ? apiResponseCode : undefined; - } - - export function getLocalizedErrorMessage(apiResponseCode: string): string { - let responseCodeInfo = getResponseCodeInformation(apiResponseCode); - return responseCodeInfo ? responseCodeInfo.message : Localization.getLocalizedString("WebClipper.Error.GenericError"); - } - - /** - * Retrieves an error message for the response returned from fetching notebooks as HTML. - */ - export function getLocalizedErrorMessageForGetNotebooks(apiResponseCode: string): string { - let fallback = Localization.getLocalizedString("WebClipper.SectionPicker.NotebookLoadUnretryableFailureMessage"); - - // Actionable codes have a message that have a hyperlink to documentation that users can use to solve their issue - let actionableResponseCodes = ["10008", "10013"]; - let responseCodeIsActionable = actionableResponseCodes.indexOf(apiResponseCode) > -1; - if (responseCodeIsActionable) { - let actionableLink = document.createElement("A") as HTMLAnchorElement; - actionableLink.href = "https://aka.ms/onapi-too-many-items-actionable"; - actionableLink.innerText = Localization.getLocalizedString("WebClipper.SectionPicker.NotebookLoadUnretryableFailureLinkMessage"); - let actionableMessageAsHtml = Localization.getLocalizedString("WebClipper.SectionPicker.NotebookLoadUnretryableFailureMessageWithExplanation") + "\n" + actionableLink.outerHTML; - return actionableMessageAsHtml; - } - - // See if there's a specific message we can show - let responseCodeInfo = getResponseCodeInformation(apiResponseCode); - if (responseCodeInfo && responseCodeInfo.message) { - return responseCodeInfo.message; - } - - // Fall back to a non-retryable message - return fallback; - } - - export function requiresSignout(apiResponseCode: string): boolean { - let responseCodeInfo = getResponseCodeInformation(apiResponseCode); - return responseCodeInfo ? responseCodeInfo.requiresSignout : false; - } - - export function isExpected(apiResponseCode: string): boolean { - let responseCodeInfo = getResponseCodeInformation(apiResponseCode); - return responseCodeInfo ? responseCodeInfo.isExpected : false; - } - - export function isRetryable(apiResponseCode: string): boolean { - let responseCodeInfo = getResponseCodeInformation(apiResponseCode); - return responseCodeInfo ? responseCodeInfo.isRetryable : false; - } - - /** - * Retrieves response code information given that the context is in POSTing a clip. - */ - function getResponseCodeInformation(apiResponseCode: string): { message: string, isRetryable: boolean, isExpected: boolean, requiresSignout?: boolean } { - let handledExtendedResponseCodes = { - "10001": { message: Localization.getLocalizedString("WebClipper.Error.GenericError"), isRetryable: true, isExpected: true }, // UnexpectedServerError - "10002": { message: Localization.getLocalizedString("WebClipper.Error.GenericError"), isRetryable: true, isExpected: true }, // ServiceUnavailable - "10004": { message: Localization.getLocalizedString("WebClipper.Error.PasswordProtected"), isRetryable: false, isExpected: true }, // PasswordProtectedSection - "10006": { message: Localization.getLocalizedString("WebClipper.Error.CorruptedSection"), isRetryable: false, isExpected: true }, // CorruptedSection - "10007": { message: Localization.getLocalizedString("WebClipper.Error.GenericError"), isRetryable: true, isExpected: true }, // ServerTooBusy - "19999": { message: Localization.getLocalizedString("WebClipper.Error.GenericError"), isRetryable: true, isExpected: false }, // GenericError - "20102": { message: Localization.getLocalizedString("WebClipper.Error.ResourceDoesNotExist"), isRetryable: false, isExpected: true }, // ResourceDoesNotExist - "30101": { message: Localization.getLocalizedString("WebClipper.Error.QuotaExceeded"), isRetryable: false, isExpected: true }, // OneDriveQuotaExceeded - "30102": { message: Localization.getLocalizedString("WebClipper.Error.SectionTooLarge"), isRetryable: false, isExpected: true }, // SectionTooLarge - "30103": { message: Localization.getLocalizedString("WebClipper.Error.GenericError"), isRetryable: true, isExpected: true }, // CoherencyFailure - "30104": { message: Localization.getLocalizedString("WebClipper.Error.UserAccountSuspended"), isRetryable: false, isExpected: true }, // UserAccountSuspended - "30105": { message: Localization.getLocalizedString("WebClipper.Error.NotProvisioned"), isRetryable: false, isExpected: true }, // OneDriveForBusinessNotProvisioned - "40004": { message: Localization.getLocalizedString("WebClipper.Error.UserDoesNotHaveUpdatePermission"), isRetryable: false, isExpected: true, requiresSignout: true } // UserOnlyHasCreatePermissions - }; - - if (!apiResponseCode) { - return; - } - return handledExtendedResponseCodes[apiResponseCode]; - } - - export function createPatchRequestBody(dataUrls: string[]): OneNoteApi.Revision[] { - let requestBody = []; - dataUrls.forEach((dataUrl) => { - let content = "

     "; - requestBody.push({ - target: "body", - action: "append", - content: content - }); - }); - return requestBody; - } - -} diff --git a/src/scripts/clipperUI/pageNav.tsx b/src/scripts/clipperUI/pageNav.tsx deleted file mode 100644 index b31b105a..00000000 --- a/src/scripts/clipperUI/pageNav.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import {Constants} from "../constants"; -import {Polyfills} from "../polyfills"; - -import {Communicator} from "../communicator/communicator"; -import {IFrameMessageHandler} from "../communicator/iframeMessageHandler"; - -import {ChangeLog} from "../versioning/changeLog"; - -import {Localization} from "../localization/localization"; - -import * as Log from "../logging/log"; -import {CommunicatorLoggerPure} from "../logging/communicatorLoggerPure"; - -import {Clipper} from "./frontEndGlobals"; -import {ComponentBase} from "./componentBase"; -import {TooltipRenderer} from "./tooltipRenderer"; -import {TooltipType} from "./tooltipType"; - -import {NewHeightInfo} from "./animations/slidingHeightAnimationStrategy"; - -export interface PageNavState { - tooltipToRender?: TooltipType; - tooltipProps?: any; -} - -/** - * Root component of the Page Nav experience. Has the responsibility of initialization, and does not - * render any UI itself. - */ -class PageNavClass extends ComponentBase { - constructor(props: {}) { - super(props); - this.initializeCommunicators(); - this.setFrontLoadedLocalizedStrings(); - this.getAndStoreDataFromExtension(); - } - - getInitialState(): PageNavState { - return {}; - } - - onClosePageNavButton() { - let closeEvent = new Log.Event.BaseEvent(Log.Event.Label.ClosePageNavTooltip); - closeEvent.setCustomProperty(Log.PropertyName.Custom.PageNavTooltipType, this.state.tooltipToRender ? TooltipType[this.state.tooltipToRender] : "unknown"); - Clipper.logger.logEvent(closeEvent); - } - - private closePageNav() { - Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.closePageNavTooltip); - } - - private getAndStoreDataFromExtension() { - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.getTooltipToRenderInPageNav, { - callback: (tooltipType: TooltipType) => { - this.setState({ tooltipToRender: tooltipType }); - } - }); - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.getPageNavTooltipProps, { - callback: (tooltipProps: any) => { - this.setState({ tooltipProps: tooltipProps }); - } - }); - } - - private initializeCommunicators() { - Clipper.setInjectCommunicator(new Communicator(new IFrameMessageHandler(() => parent), Constants.CommunicationChannels.pageNavInjectedAndPageNavUi)); - Clipper.setExtensionCommunicator(new Communicator(new IFrameMessageHandler(() => parent), Constants.CommunicationChannels.extensionAndPageNavUi)); - Clipper.logger = new CommunicatorLoggerPure(Clipper.getExtensionCommunicator()); - } - - private setFrontLoadedLocalizedStrings() { - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.clipperStringsFrontLoaded, { - callback: (locStringsObj) => { - Localization.setLocalizedStrings(locStringsObj); - } - }); - } - - private updateFrameHeight(newHeightInfo: NewHeightInfo) { - Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.updateFrameHeight, { - param: newHeightInfo.newContainerHeight - }); - } - - render() { - return ( - - ); - } -} - -Polyfills.init(); - -// Catch any unhandled exceptions and log them -let oldOnError = self.onerror; -self.onerror = (message: string, filename?: string, lineno?: number, colno?: number, error?: Error) => { - let callStack = error ? Log.Failure.getStackTrace(error) : "[unknown stacktrace]"; - - Clipper.logger.logFailure(Log.Failure.Label.UnhandledExceptionThrown, Log.Failure.Type.Unexpected, - { error: message + " (" + filename + ":" + lineno + ":" + colno + ") at " + callStack }, "PageNavUI"); - - if (oldOnError) { - oldOnError(message, filename, lineno, colno, error); - } -}; - -let component = PageNavClass.componentize(); -m.mount(document.getElementById("pageNavUIPlaceholder"), component); -export {component as PageNav} diff --git a/src/scripts/clipperUI/panels/changeLogPanel.tsx b/src/scripts/clipperUI/panels/changeLogPanel.tsx deleted file mode 100644 index c8bf2877..00000000 --- a/src/scripts/clipperUI/panels/changeLogPanel.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import {Constants} from "../../constants"; - -import {Localization} from "../../localization/localization"; - -import {ChangeLog} from "../../versioning/changeLog"; - -import {Clipper} from "../frontEndGlobals"; -import {ComponentBase} from "../componentBase"; -import {TooltipProps} from "../tooltipProps"; - -class ChangeLogPanelClass extends ComponentBase<{}, TooltipProps.WhatsNew> { - getInitialState(): {} { - return {}; - } - - createChangeElement(change: ChangeLog.Change) { - let image = change.imageUrl ? - : - undefined; - - return ( -
    - {image ? -
    - {image} -
    : - undefined - } -
    -
    - - {change.title} - -
    -
    - - {change.description} - -
    -
    -
    - ); - } - - getChangeElements() { - let changeElements = []; - for (let i = 0; i < this.props.updates.length; i++) { - for (let j = 0; j < this.props.updates[i].changes.length; j++) { - changeElements.push(this.createChangeElement(this.props.updates[i].changes[j])); - } - } - return changeElements; - } - - render() { - return ( -
    - {this.getChangeElements()} -
    - ); - } -} - -let component = ChangeLogPanelClass.componentize(); -export {component as ChangeLogPanel} diff --git a/src/scripts/clipperUI/panels/clippingPanel.tsx b/src/scripts/clipperUI/panels/clippingPanel.tsx deleted file mode 100644 index 6fc5f2a3..00000000 --- a/src/scripts/clipperUI/panels/clippingPanel.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import {Constants} from "../../constants"; - -import {AugmentationModel} from "../../contentCapture/augmentationHelper"; - -import {ExtensionUtils} from "../../extensions/extensionUtils"; - -import {Localization} from "../../localization/localization"; - -import * as Log from "../../logging/log"; - -import {ClipMode} from "../clipMode"; -import {Clipper} from "../frontEndGlobals"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; -import {Status} from "../status"; - -import {SpriteAnimation} from "../components/spriteAnimation"; - -class ClippingPanelClass extends ComponentBase<{}, ClipperStateProp> { - getProgressLabel(): string { - switch (this.props.clipperState.currentMode.get()) { - case ClipMode.Pdf: - return Localization.getLocalizedString("WebClipper.ClipType.Pdf.ProgressLabel"); - case ClipMode.Region: - return Localization.getLocalizedString("WebClipper.ClipType.Region.ProgressLabel"); - case ClipMode.Augmentation: - let retVal: string; - try { - let augmentationModelStr = AugmentationModel[this.props.clipperState.augmentationResult.data.ContentModel]; - retVal = Localization.getLocalizedString("WebClipper.ClipType." + augmentationModelStr + ".ProgressLabel"); - } catch (e) { - Clipper.logger.logFailure(Log.Failure.Label.GetLocalizedString, Log.Failure.Type.Unexpected, - { error: e.message }, ClipMode[this.props.clipperState.currentMode.get()]); - // Fallback string - retVal = Localization.getLocalizedString("WebClipper.ClipType.ScreenShot.ProgressLabel"); - } - return retVal; - case ClipMode.Bookmark: - return Localization.getLocalizedString("WebClipper.ClipType.Bookmark.ProgressLabel"); - case ClipMode.Selection: - return Localization.getLocalizedString("WebClipper.ClipType.Selection.ProgressLabel"); - default: - case ClipMode.FullPage: - return Localization.getLocalizedString("WebClipper.ClipType.ScreenShot.ProgressLabel"); - } - } - - render() { - return ( -
    - - - {this.getProgressLabel()} - -
    - ); - } -} - -let component = ClippingPanelClass.componentize(); -export {component as ClippingPanel}; diff --git a/src/scripts/clipperUI/panels/clippingPanelWithDelayedMessage.tsx b/src/scripts/clipperUI/panels/clippingPanelWithDelayedMessage.tsx deleted file mode 100644 index 9bb72445..00000000 --- a/src/scripts/clipperUI/panels/clippingPanelWithDelayedMessage.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import {Constants} from "../../constants"; - -import {Localization} from "../../localization/localization"; - -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; - -import {ClippingPanel} from "./clippingPanel"; - -interface ClippingPanelWithDelayedMessageState { - showMessage: boolean; -} - -interface ClippingPanelWithDelayedMessageProp extends ClipperStateProp { - delay: number; - message: number; -} - -class ClippingPanelWithDelayedMessageClass extends ComponentBase { - constructor(props: ClippingPanelWithDelayedMessageProp) { - super(props); - setTimeout(() => { - this.setState({ - showMessage: true - }); - }, Math.max(this.props.delay, 0)); - } - - getInitialState(): ClippingPanelWithDelayedMessageState { - return { - showMessage: false - }; - } - - getMessageElement() { - return ( - - {this.props.message} - - ); - } - - render() { - return ( -
    - - { this.state.showMessage ? this.getMessageElement() : undefined} -
    - ); - } -} - -let component = ClippingPanelWithDelayedMessageClass.componentize(); -export {component as ClippingPanelWithDelayedMessage}; diff --git a/src/scripts/clipperUI/panels/clippingPanelWithProgressIndicator.tsx b/src/scripts/clipperUI/panels/clippingPanelWithProgressIndicator.tsx deleted file mode 100644 index a125b714..00000000 --- a/src/scripts/clipperUI/panels/clippingPanelWithProgressIndicator.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {Constants} from "../../constants"; -import {ObjectUtils} from "../../objectUtils"; - -import {Localization} from "../../localization/localization"; - -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; - -import {ClippingPanel} from "./clippingPanel"; - -class ClippingPanelWithProgressIndicatorClass extends ComponentBase<{}, ClipperStateProp> { - getMessageElement() { - const pdfProgress = this.props.clipperState.clipSaveStatus; - - // We would rather show no message at all, than show a broken message like "Clipping page -1 of -5..."" - if (ObjectUtils.isNullOrUndefined(pdfProgress.numItemsCompleted) || ObjectUtils.isNullOrUndefined(pdfProgress.numItemsTotal)) { - return undefined; - } else if (pdfProgress.numItemsCompleted > pdfProgress.numItemsTotal) { - return undefined; - } else if (pdfProgress.numItemsCompleted < 0 || pdfProgress.numItemsTotal < 0) { - return undefined; - } - - let message = Localization.getLocalizedString("WebClipper.ClipType.Pdf.IncrementalProgressMessage").replace("{0}", (pdfProgress.numItemsCompleted + 1).toString()); - message = message.replace("{1}", pdfProgress.numItemsTotal.toString()); - - return ( - - {message} - - ); - } - - render() { - return ( -
    - - {this.getMessageElement()} -
    - ); - } -} - -let component = ClippingPanelWithProgressIndicatorClass.componentize(); -export {component as ClippingPanelWithProgressIndicator}; diff --git a/src/scripts/clipperUI/panels/dialogPanel.tsx b/src/scripts/clipperUI/panels/dialogPanel.tsx deleted file mode 100644 index 7b351d29..00000000 --- a/src/scripts/clipperUI/panels/dialogPanel.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import {ComponentBase} from "../componentBase"; -import {AnimationStrategy} from "../animations/animationStrategy"; - -import {Constants} from "../../constants"; -import {ObjectUtils} from "../../objectUtils"; - -import {Localization} from "../../localization/localization"; - -export interface DialogButton { - id: string; - label: string; - handler: () => void; -} - -export interface DialogPanelProps { - message: string; - content: HTMLElement[]; - buttons: DialogButton[]; - fontFamily?: Localization.FontFamily; - buttonFontFamily?: Localization.FontFamily; - containerId?: string; - panelAnimationStrategy?: AnimationStrategy; -} - -export abstract class DialogPanelClass extends ComponentBase<{}, DialogPanelProps> { - public getExtraMessages(): any { - return undefined; - } - - private onPanelAnimatorDraw(panelAnimator: HTMLElement) { - if (this.props.panelAnimationStrategy) { - this.props.panelAnimationStrategy.animate(panelAnimator); - } - } - - render() { - let fontFamily = !ObjectUtils.isNullOrUndefined(this.props.fontFamily) ? this.props.fontFamily : Localization.FontFamily.Semibold; - let buttonFontFamily = !ObjectUtils.isNullOrUndefined(this.props.buttonFontFamily) ? this.props.buttonFontFamily : Localization.FontFamily.Semibold; - let containerId = this.props.containerId ? this.props.containerId : ""; - - return ( -
    -
    -
    -
    -
    - {this.props.message} -
    - {this.getExtraMessages()} -
    -
    -
    - {this.props.content} -
    -
    - {this.props.buttons.map((button, i) => { - return ( -
    - - { button.label } - -
    - ); - }) } -
    -
    -
    - ); - } -} - -let component = DialogPanelClass.componentize(); -export {component as DialogPanel}; diff --git a/src/scripts/clipperUI/panels/errorDialogPanel.tsx b/src/scripts/clipperUI/panels/errorDialogPanel.tsx deleted file mode 100644 index ede03762..00000000 --- a/src/scripts/clipperUI/panels/errorDialogPanel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {Constants} from "../../constants"; - -import {Clipper} from "../frontEndGlobals"; - -import {DialogPanelClass} from "./dialogPanel"; - -class ErrorDialogPanelClass extends DialogPanelClass { - // Override - public getExtraMessages(): any { - return ( -
    - {this.getDebugSessionId()} -
    - ); - } - - private getDebugSessionId(): string { - return "Usid: " + Clipper.getUserSessionId(); - } -} - -let component = ErrorDialogPanelClass.componentize(); -export {component as ErrorDialogPanel}; diff --git a/src/scripts/clipperUI/panels/loadingPanel.tsx b/src/scripts/clipperUI/panels/loadingPanel.tsx deleted file mode 100644 index 05c5520e..00000000 --- a/src/scripts/clipperUI/panels/loadingPanel.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import {Constants} from "../../constants"; -import {ExtensionUtils} from "../../extensions/extensionUtils"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; - -import {SpriteAnimation} from "../components/spriteAnimation"; - -class LoadingPanelClass extends ComponentBase<{ }, ClipperStateProp> { - render() { - return ( -
    - -
    - ); - } -} - -let component = LoadingPanelClass.componentize(); -export {component as LoadingPanel}; diff --git a/src/scripts/clipperUI/panels/optionsPanel.tsx b/src/scripts/clipperUI/panels/optionsPanel.tsx deleted file mode 100644 index 780e814d..00000000 --- a/src/scripts/clipperUI/panels/optionsPanel.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import {Constants} from "../../constants"; -import {OperationResult} from "../../operationResult"; -import {StringUtils} from "../../stringUtils"; -import {Localization} from "../../localization/localization"; -import {ModeButtonSelector} from "../components/modeButtonSelector"; -import {SectionPicker} from "../components/sectionPicker"; -import {ClipMode} from "../clipMode"; -import {ClipperStateProp} from "../clipperState"; -import {ClipperStateUtilities} from "../clipperStateUtilities"; -import {ComponentBase} from "../componentBase"; -import {Status} from "../status"; -import {PdfClipOptions} from "../components/pdfClipOptions"; -import * as _ from "lodash"; - -export interface OptionsPanelProp extends ClipperStateProp { - onStartClip: () => void; - onPopupToggle: (shouldNowBeOpen: boolean) => void; -} - -class OptionsPanelClass extends ComponentBase<{}, OptionsPanelProp> { - getCurrentClippingOptions(): any { - const currentMode = this.props.clipperState.currentMode.get(); - switch (currentMode) { - case ClipMode.Pdf: - return ; - default: - return undefined; - } - } - - checkOptionsBeforeStartClip() { - const clipperState = this.props.clipperState; - const pdfPreviewInfo = clipperState.pdfPreviewInfo; - - // If the user is in PDF mode and selected a page range, disallow the clip if it's an invalid range - if (clipperState.currentMode.get() === ClipMode.Pdf && !pdfPreviewInfo.allPages && clipperState.pdfResult.status === Status.Succeeded) { - const parsePageRangeOperation = StringUtils.parsePageRange(pdfPreviewInfo.selectedPageRange, clipperState.pdfResult.data.get().pdf.numPages()); - if (parsePageRangeOperation.status !== OperationResult.Succeeded) { - let newPdfPreviewInfo = _.extend(_.cloneDeep(pdfPreviewInfo), { shouldShowPopover: true }); - clipperState.setState({ - pdfPreviewInfo: newPdfPreviewInfo - }); - return; - } - } - - this.props.onStartClip(); - } - - // This function is passed into Mithril's config property - // The config property is a function that allows you to hook into - // a component's lifecycle such as mounting and un-mounting - attachClipHotKeyListener(element, isInitialized, context) { - // If this is the first time we are initializing this element, - // then attach the listener - if (!isInitialized) { - let oldOnKeyDown = document.onkeydown; - document.onkeydown = (ev: KeyboardEvent) => { - // TODO: KeyboardEvent::which is deprecated but PhantomJs doesn't support - // 'event constructors', which is necessary to use the recommended KeyboardEvent::key - if (ev.altKey && ev.which === Constants.KeyCodes.c) { - this.checkOptionsBeforeStartClip(); - } - - // Prevent focus on Edge+Narrator from going into the background page by looping focus on our page - if (ev.shiftKey && ev.keyCode === Constants.KeyCodes.tab) { - const targetAsAny = ev.target as any; - if (targetAsAny && targetAsAny.id) { - const idsThatShouldLoopFocus = [ - Constants.Ids.fullPageButton, - Constants.Ids.bookmarkButton, - Constants.Ids.regionButton, - Constants.Ids.augmentationButton - ]; - if (idsThatShouldLoopFocus.indexOf(targetAsAny.id) !== -1) { - document.getElementById("closeButton").focus(); - ev.preventDefault(); - ev.stopImmediatePropagation(); - return; - } - } - } - if (oldOnKeyDown) { - oldOnKeyDown.call(document, ev); - } - }; - - // Remove listener when this element is unmounted - context.onunload = () => { - document.onkeydown = oldOnKeyDown ? oldOnKeyDown.bind(document) : undefined; - }; - // There is no else case, since we only care about initializaiton and destruction - } - } - - render() { - let clipButtonEnabled = ClipperStateUtilities.clipButtonEnabled(this.props.clipperState); - let clipButtonContainerClassName = clipButtonEnabled ? "wideButtonContainer" : "wideButtonContainer disabled"; - let clippingOptionsToRender = this.getCurrentClippingOptions(); - - return ( -
    - - {clippingOptionsToRender} - -
    - {clipButtonEnabled - ? - - {Localization.getLocalizedString("WebClipper.Action.Clip")} - - - : - - {Localization.getLocalizedString("WebClipper.Action.Clip")} - - } -
    -
    - ); - } -} - -let component = OptionsPanelClass.componentize(); -export {component as OptionsPanel}; diff --git a/src/scripts/clipperUI/panels/ratingsPanel.tsx b/src/scripts/clipperUI/panels/ratingsPanel.tsx deleted file mode 100644 index 1b620278..00000000 --- a/src/scripts/clipperUI/panels/ratingsPanel.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import {ClientType} from "../../clientType"; -import {Constants} from "../../constants"; -import {ObjectUtils} from "../../objectUtils"; - -import {SmartValue} from "../../communicator/smartValue"; - -import {Localization} from "../../localization/localization"; - -import * as Log from "../../logging/log"; - -import {ClipperStorageKeys} from "../../storage/clipperStorageKeys"; - -import {ClipMode} from "../clipMode"; -import {ClipperState, ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; -import {Clipper} from "../frontEndGlobals"; -import {RatingsHelper, RatingsPromptStage} from "../ratingsHelper"; -import {Status} from "../status"; - -import {AnimationState} from "../animations/animationState"; -import {AnimationStrategy} from "../animations/animationStrategy"; -import {SlideContentInFromTopAnimationStrategy, ContentToAnimate} from "../animations/slideContentInFromTopAnimationStrategy"; - -import {SpriteAnimation} from "../components/spriteAnimation"; - -import {DialogButton, DialogPanel} from "./dialogPanel"; - -interface RatingsPanelState { - currentRatingsPromptStage?: RatingsPromptStage; - userSelectedRatingsPromptStage?: RatingsPromptStage; -} - -interface RatingsPanelProp extends ClipperStateProp { - animationState: SmartValue; -} - -class RatingsPanelClass extends ComponentBase { - getInitialState(): RatingsPanelState { - return { - currentRatingsPromptStage: RatingsPromptStage.Init - }; - } - - /** - * Get the panel animation strategy for the ratings subpanel of the success panel provided - */ - private getPanelAnimationStrategy(panel: RatingsPanelClass): AnimationStrategy { - if (this.props.animationState) { - return new SlideContentInFromTopAnimationStrategy({ - currentAnimationState: this.props.animationState, - contentToAnimate: this.getContentToAnimate(), - extShouldAnimateIn: () => { - return (ObjectUtils.isNullOrUndefined(panel.state.userSelectedRatingsPromptStage) || - panel.state.userSelectedRatingsPromptStage === panel.state.currentRatingsPromptStage); - }, - extShouldAnimateOut: () => { - return panel.state.userSelectedRatingsPromptStage > panel.state.currentRatingsPromptStage; - }, - onAfterAnimateOut: () => { panel.setState({ currentRatingsPromptStage: panel.state.userSelectedRatingsPromptStage }); } - }); - } - } - - private getContentToAnimate(): ContentToAnimate[] { - return [ - { - cssSelector: ".messageLabel", - animateInOptions: { - slideDownDeltas: [50], - delaysInMs: [33] - } - }, - { - cssSelector: ".dialogButton.wideButtonContainer", - animateInOptions: { - slideDownDeltas: [48, 48], - delaysInMs: [50, 0] - } - } - ]; - } - - /** - * Get appropriate dialog panel message for the ratings prompt stage provided - */ - private getMessage(stage: RatingsPromptStage): string { - switch (stage) { - case RatingsPromptStage.Init: - return Localization.getLocalizedString("WebClipper.Label.Ratings.Message.Init"); - case RatingsPromptStage.Rate: - return Localization.getLocalizedString("WebClipper.Label.Ratings.Message.Rate"); - case RatingsPromptStage.Feedback: - return Localization.getLocalizedString("WebClipper.Label.Ratings.Message.Feedback"); - case RatingsPromptStage.End: - return Localization.getLocalizedString("WebClipper.Label.Ratings.Message.End"); - default: - case RatingsPromptStage.None: - return; - } - } - - /** - * Get appropriate dialog panel buttons for the panel (with its internal states) provided - */ - private getDialogButtons(panel: RatingsPanelClass): DialogButton[] { - let stage: RatingsPromptStage = panel.state.currentRatingsPromptStage; - let clipperState: ClipperState = panel.props.clipperState; - let clientType: ClientType = clipperState.clientInfo.clipperType; - - let buttons: DialogButton[] = []; - - switch (stage) { - case RatingsPromptStage.Init: - buttons.push({ - id: Constants.Ids.ratingsButtonInitYes, - label: Localization.getLocalizedString("WebClipper.Label.Ratings.Button.Init.Positive"), - handler: () => { - RatingsHelper.setDoNotPromptStatus(); - - let rateUrl: string = RatingsHelper.getRateUrlIfExists(clientType); - if (rateUrl) { - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.Rate - }); - } else { - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.End - }); - } - - this.forceTransitionIfAnimationsAreOff(panel); - } - }, { - id: Constants.Ids.ratingsButtonInitNo, - label: Localization.getLocalizedString("WebClipper.Label.Ratings.Button.Init.Negative"), - handler: () => { - if (RatingsHelper.badRatingAlreadyOccurred()) { - // setting this to prevent additional ratings prompts after the second bad rating - RatingsHelper.setDoNotPromptStatus(); - } - - let lastSeenVersion: string = Clipper.getCachedValue(ClipperStorageKeys.lastSeenVersion); - Clipper.storeValue(ClipperStorageKeys.lastBadRatingDate, Date.now().toString()); - Clipper.storeValue(ClipperStorageKeys.lastBadRatingVersion, lastSeenVersion); - - let feedbackUrl: string = RatingsHelper.getFeedbackUrlIfExists(clipperState); - if (feedbackUrl) { - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.Feedback - }); - } else { - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.End - }); - } - - this.forceTransitionIfAnimationsAreOff(panel); - } - }); - break; - case RatingsPromptStage.Rate: - let rateUrl: string = RatingsHelper.getRateUrlIfExists(clientType); - if (rateUrl) { - buttons.push({ - id: Constants.Ids.ratingsButtonRateYes, - label: Localization.getLocalizedString("WebClipper.Label.Ratings.Button.Rate"), - handler: () => { - window.open(rateUrl, "_blank"); - - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.End - }); - - this.forceTransitionIfAnimationsAreOff(panel); - } - }, { - id: Constants.Ids.ratingsButtonRateNo, - label: Localization.getLocalizedString("WebClipper.Label.Ratings.Button.NoThanks"), - handler: () => { - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.None - }); - - this.forceTransitionIfAnimationsAreOff(panel); - } - }); - } else { - // this shouldn't happen - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.None - }); - } - break; - case RatingsPromptStage.Feedback: - let feedbackUrl: string = RatingsHelper.getFeedbackUrlIfExists(clipperState); - if (feedbackUrl) { - buttons.push({ - id: Constants.Ids.ratingsButtonFeedbackYes, - label: Localization.getLocalizedString("WebClipper.Label.Ratings.Button.Feedback"), - handler: () => { - window.open(feedbackUrl, "_blank"); - - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.End - }); - - this.forceTransitionIfAnimationsAreOff(panel); - } - }, { - id: Constants.Ids.ratingsButtonFeedbackNo, - label: Localization.getLocalizedString("WebClipper.Label.Ratings.Button.NoThanks"), - handler: () => { - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.None - }); - - this.forceTransitionIfAnimationsAreOff(panel); - } - }); - } else { - // this shouldn't happen - panel.setState({ - userSelectedRatingsPromptStage: RatingsPromptStage.None - }); - } - break; - default: - case RatingsPromptStage.End: - case RatingsPromptStage.None: - break; - } - - return buttons; - } - - private forceTransitionIfAnimationsAreOff(panel: RatingsPanelClass) { - if (!panel.props.animationState) { - panel.setState({ currentRatingsPromptStage: panel.state.userSelectedRatingsPromptStage }); - } - } - - render() { - if (!this.props.clipperState.showRatingsPrompt) { - return
    ; - } - - let message: string = this.getMessage(this.state.currentRatingsPromptStage); - - if (!ObjectUtils.isNullOrUndefined(message)) { - let buttons: DialogButton[] = this.getDialogButtons(this); - let panelAnimationStrategy = this.getPanelAnimationStrategy(this); - - return ( - - ); - } - - return
    ; - } -} - -let component = RatingsPanelClass.componentize(); -export {component as RatingsPanel}; diff --git a/src/scripts/clipperUI/panels/regionSelectingPanel.tsx b/src/scripts/clipperUI/panels/regionSelectingPanel.tsx deleted file mode 100644 index b5c5dee9..00000000 --- a/src/scripts/clipperUI/panels/regionSelectingPanel.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import {Constants} from "../../constants"; -import {Localization} from "../../localization/localization"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; - -class RegionSelectingPanelClass extends ComponentBase<{}, ClipperStateProp> { - initiallySetFocus(element: HTMLElement) { - element.focus(); - } - - handleCancelButton() { - this.props.clipperState.setState({ - focusOnRender: Constants.Ids.regionButton - }); - this.props.clipperState.reset(); - } - - render() { - return ( -
    -
    -
    -
    - {Localization.getLocalizedString("WebClipper.Label.RegionSelectionMouseInstruction")} - {Localization.getLocalizedString("WebClipper.Label.RegionSelectionKeyboardInstruction")} -
    - - {Localization.getLocalizedString("WebClipper.Label.RegionSelectionMouseInstruction")} -
    - {Localization.getLocalizedString("WebClipper.Label.RegionSelectionKeyboardInstruction")} -
    -
    -
    - - - {Localization.getLocalizedString("WebClipper.Action.BackToHome")} - - -
    -
    -
    - ); - } -} - -let component = RegionSelectingPanelClass.componentize(); -export {component as RegionSelectingPanel}; diff --git a/src/scripts/clipperUI/panels/signInPanel.tsx b/src/scripts/clipperUI/panels/signInPanel.tsx deleted file mode 100644 index e8818e3b..00000000 --- a/src/scripts/clipperUI/panels/signInPanel.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import {ClientType} from "../../clientType"; -import {Constants} from "../../constants"; -import {AuthType, UpdateReason} from "../../userInfo"; - -import {ExtensionUtils} from "../../extensions/extensionUtils"; - -import {Localization} from "../../localization/localization"; - -import {Clipper} from "../frontEndGlobals"; -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; - -interface SignInPanelState { - debugInformationShowing?: boolean; -} - -interface SignInPanelProps extends ClipperStateProp { - onSignInInvoked: (authType: AuthType) => void; -} - -class SignInPanelClass extends ComponentBase { - initiallySetFocus(element: HTMLElement) { - element.focus(); - } - - getInitialState() { - return { debugInformationShowing: false }; - } - - onSignInMsa() { - this.props.onSignInInvoked(AuthType.Msa); - } - - onSignInOrgId() { - this.props.onSignInInvoked(AuthType.OrgId); - } - - getSignInDescription(): string { - // Since the user is not signed in, we show a message depending on the reason of the last update to the userResult - if (!this.props.clipperState.userResult || !this.props.clipperState.userResult.data) { - return Localization.getLocalizedString("WebClipper.Label.SignInDescription"); - } - - switch (this.props.clipperState.userResult.data.updateReason) { - case UpdateReason.SignInAttempt: - return Localization.getLocalizedString("WebClipper.Error.SignInUnsuccessful"); - case UpdateReason.TokenRefreshForPendingClip: - return Localization.getLocalizedString("WebClipper.Error.GenericExpiredTokenRefreshError"); - default: - case UpdateReason.SignInCancel: - case UpdateReason.SignOutAction: - case UpdateReason.InitialRetrieval: - return Localization.getLocalizedString("WebClipper.Label.SignInDescription"); - } - } - - signInAttempted(): boolean { - return !!this.props.clipperState.userResult && !!this.props.clipperState.userResult.data - && this.props.clipperState.userResult.data.updateReason === UpdateReason.SignInAttempt; - } - - signInFailureContainsErrorDescription(): boolean { - return this.signInAttempted() - && this.props.clipperState.userResult.data.errorDescription - // Right now we are only showing the error panel for OrgId errors since they tend to - // be a little more actionable to the user, or at least a little more helpful. - && this.props.clipperState.userResult.data.errorDescription.indexOf("OrgId") === 0; - } - - signInFailureThirdPartyCookiesBlocked(): boolean { - return this.signInAttempted() - && !this.props.clipperState.userResult.data.user - && !this.props.clipperState.userResult.data.writeableCookies; - } - - debugInformationControlHandler() { - this.setState({ debugInformationShowing: !this.state.debugInformationShowing }); - } - - signInErrorDetected() { - return this.signInFailureContainsErrorDescription() || this.signInFailureThirdPartyCookiesBlocked(); - } - - errorMoreInformationTogggle() { - if (this.signInErrorDetected()) { - return
    - - - - {this.state.debugInformationShowing - ? Localization.getLocalizedString("WebClipper.Label.SignInUnsuccessfulLessInformation") - : Localization.getLocalizedString("WebClipper.Label.SignInUnsuccessfulMoreInformation") - } - - - {this.debugInformation()} -
    ; - } - - return undefined; - } - - debugInformation() { - if (this.signInErrorDetected() && this.state.debugInformationShowing) { - return
    -
    -
      -
    • {ClientType[this.props.clipperState.clientInfo.clipperType]}: {this.props.clipperState.clientInfo.clipperVersion}
    • -
    • ID: {this.props.clipperState.clientInfo.clipperId}
    • -
    • USID: {Clipper.getUserSessionId()}
    • -
    -
    -
    ; - } - } - - getErrorDescription() { - if (this.signInFailureContainsErrorDescription()) { - return this.props.clipperState.userResult.data.errorDescription; - } else if (this.signInFailureThirdPartyCookiesBlocked()) { - - let browserSpecificMessage = ""; - switch (this.props.clipperState.clientInfo.clipperType) { - case ClientType.ChromeExtension: - browserSpecificMessage = Localization.getLocalizedString("WebClipper.Error.CookiesDisabled.Chrome"); - break; - case ClientType.EdgeExtension: - browserSpecificMessage = Localization.getLocalizedString("WebClipper.Error.CookiesDisabled.Edge"); - break; - case ClientType.FirefoxExtension: - browserSpecificMessage = Localization.getLocalizedString("WebClipper.Error.CookiesDisabled.Firefox"); - break; - default: - browserSpecificMessage = Localization.getLocalizedString("WebClipper.Error.CookiesDisabled.Line2"); - break; - } - - return
    -
    {Localization.getLocalizedString("WebClipper.Error.CookiesDisabled.Line1")}
    -
    {browserSpecificMessage}
    -
    ; - } - - return undefined; - } - - errorInformationDescription() { - if (this.signInErrorDetected()) { - return
    - - {this.getErrorDescription()} - - {this.errorMoreInformationTogggle()} -
    ; - } - - return undefined; - } - - render() { - return ( -
    -
    - -
    - - {Localization.getLocalizedString("WebClipper.Label.OneNoteClipper")} - -
    -
    - - {this.getSignInDescription()} - -
    - {this.errorInformationDescription()} -
    - - {Localization.getLocalizedString("WebClipper.Action.SigninMsa")} - -
    -
    - - {Localization.getLocalizedString("WebClipper.Action.SigninOrgId") } - -
    -
    -
    - ); - } -} - -let component = SignInPanelClass.componentize(); -export {component as SignInPanel}; diff --git a/src/scripts/clipperUI/panels/successPanel.tsx b/src/scripts/clipperUI/panels/successPanel.tsx deleted file mode 100644 index ba08ed2a..00000000 --- a/src/scripts/clipperUI/panels/successPanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import {Constants} from "../../constants"; -import {UrlUtils} from "../../urlUtils"; - -import {ExtensionUtils} from "../../extensions/extensionUtils"; - -import {Localization} from "../../localization/localization"; - -import * as Log from "../../logging/log"; - -import {ClipperStateProp} from "../clipperState"; -import {ComponentBase} from "../componentBase"; -import {Clipper} from "../frontEndGlobals"; - -import {SpriteAnimation} from "../components/spriteAnimation"; - -class SuccessPanelClass extends ComponentBase<{ }, ClipperStateProp> { - initiallySetFocus(element: HTMLElement) { - element.focus(); - } - - public onLaunchOneNoteButton() { - Clipper.logger.logUserFunnel(Log.Funnel.Label.ViewInWac); - let data = this.props.clipperState.oneNoteApiResult.data as OneNoteApi.Page; - if (data && data.links && data.links.oneNoteWebUrl && data.links.oneNoteWebUrl.href) { - let urlWithFromClipperParam = UrlUtils.addUrlQueryValue(data.links.oneNoteWebUrl.href, Constants.Urls.QueryParams.wdFromClipper, "1"); - window.open(urlWithFromClipperParam, "_blank"); - } else { - Clipper.logger.logFailure(Log.Failure.Label.OnLaunchOneNoteButton, Log.Failure.Type.Unexpected, - { error: "Page created and returned by API is either missing entirely, or missing its links, oneNoteWebUrl, or oneNoteWebUrl.href. Page: " + data }); - } - } - - render() { - return ( -
    -
    - - - {Localization.getLocalizedString("WebClipper.Label.ClipSuccessful")} - -
    -
    - - {Localization.getLocalizedString("WebClipper.Action.ViewInOneNote")} - -
    -
    - ); - } -} - -let component = SuccessPanelClass.componentize(); -export {component as SuccessPanel}; diff --git a/src/scripts/clipperUI/previewViewer.tsx b/src/scripts/clipperUI/previewViewer.tsx deleted file mode 100644 index b37deee6..00000000 --- a/src/scripts/clipperUI/previewViewer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import {ClientType} from "../clientType"; - -import {Localization} from "../localization/localization"; - -import {ClipMode} from "./clipMode"; -import {ClipperStateProp} from "./clipperState"; -import {ComponentBase} from "./componentBase"; - -import {AugmentationPreview} from "./components/previewViewer/augmentationPreview"; -import {BookmarkPreview} from "./components/previewViewer/bookmarkPreview"; -import {FullPagePreview} from "./components/previewViewer/fullPagePreview"; -import {PdfPreview} from "./components/previewViewer/pdfPreview"; -import {RegionPreview} from "./components/previewViewer/regionPreview"; -import {SelectionPreview} from "./components/previewViewer/selectionPreview"; -import {LocalFilesNotAllowedPanel} from "./components/previewViewer/localFilesNotAllowedPanel"; - -class PreviewViewerClass extends ComponentBase { - render() { - let state = this.props.clipperState; - switch (state.currentMode.get()) { - case ClipMode.Pdf: - if (!state.pdfPreviewInfo.isLocalFileAndNotAllowed) { - if (state.clientInfo.clipperType === ClientType.ChromeExtension) { - return ; - } - return ; - } - return ; - case ClipMode.FullPage: - return ; - case ClipMode.Region: - return ; - case ClipMode.Augmentation: - return ; - case ClipMode.Bookmark: - return ; - case ClipMode.Selection: - return ; - default: - return
    ; - } - } -} - -let component = PreviewViewerClass.componentize(); -export {component as PreviewViewer}; diff --git a/src/scripts/clipperUI/ratingsHelper.ts b/src/scripts/clipperUI/ratingsHelper.ts deleted file mode 100644 index adc0c5db..00000000 --- a/src/scripts/clipperUI/ratingsHelper.ts +++ /dev/null @@ -1,345 +0,0 @@ -import {ClientType} from "../clientType"; -import {ClipperUrls} from "../clipperUrls"; -import {Constants} from "../constants"; -import {ObjectUtils} from "../objectUtils"; -import {Settings} from "../settings"; - -import * as Log from "../logging/log"; - -import {ClipperStorageKeys} from "../storage/clipperStorageKeys"; - -import {Version} from "../versioning/version"; - -import {ClipperState} from "./clipperState"; -import {Clipper} from "./frontEndGlobals"; - -// ordered by stage progression -export enum RatingsPromptStage { - Init = 0, - Rate = 1, - Feedback = 2, - End = 3, - None = 4 -} - -interface RatingsLoggingInfo { - badRatingTimingDelayIsOver?: boolean; - badRatingVersionDelayIsOver?: boolean; - clipSuccessDelayIsOver?: boolean; - doNotPromptRatings?: boolean; - lastBadRatingDate?: string; - lastBadRatingVersion?: string; - lastSeenVersion?: string; - numSuccessfulClips?: number; - numSuccessfulClipsAnchor?: number; - ratingsPromptEnabledForClient?: boolean; - usedCachedValue?: boolean; -} - -export class RatingsHelper { - public static rateUrlSettingNameSuffix = "_RatingUrl"; - public static ratingsPromptEnabledSettingNameSuffix = "_RatingsEnabled"; - - /** - * Returns true if ClipperStorageKeys.lastBadRatingDate already contained a cached value - * (meaning the user had already rated us negatively) - */ - public static badRatingAlreadyOccurred(): boolean { - let lastBadRatingDateAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.lastBadRatingDate); - let lastBadRatingDate: number = parseInt(lastBadRatingDateAsStr, 10); - if (!isNaN(lastBadRatingDate) && RatingsHelper.isValidDate(lastBadRatingDate)) { - return true; - } - return false; - } - - /** - * Get the feedback URL with the special ratings prompt log category, if it exists - */ - public static getFeedbackUrlIfExists(clipperState: ClipperState): string { - let ratingsPromptLogCategory: string = Settings.getSetting("LogCategory_RatingsPrompt"); - if (!ObjectUtils.isNullOrUndefined(ratingsPromptLogCategory) && ratingsPromptLogCategory.length > 0) { - return ClipperUrls.generateFeedbackUrl(clipperState, Clipper.getUserSessionId(), ratingsPromptLogCategory); - } - } - - /** - * Get ratings/reviews URL for the provided ClientType/ClipperType, if it exists - */ - public static getRateUrlIfExists(clientType: ClientType): string { - let settingName: string = RatingsHelper.getRateUrlSettingNameForClient(clientType); - return Settings.getSetting(settingName); - } - - /** - * Pre-cache values needed to determine whether to show the ratings prompt - */ - public static preCacheNeededValues(): void { - let ratingsPromptStorageKeys = [ - ClipperStorageKeys.doNotPromptRatings, - ClipperStorageKeys.isRatingsPromptLogicExecutedInEdge, - ClipperStorageKeys.lastBadRatingDate, - ClipperStorageKeys.lastBadRatingVersion, - ClipperStorageKeys.lastSeenVersion, - ClipperStorageKeys.numSuccessfulClips, - ClipperStorageKeys.numSuccessfulClipsRatingsEnablement - ]; - Clipper.preCacheStoredValues(ratingsPromptStorageKeys); - } - - /** - * Set ClipperStorageKeys.doNotPromptRatings value to "true" - */ - public static setDoNotPromptStatus(): void { - Clipper.storeValue(ClipperStorageKeys.doNotPromptRatings, "true"); - - Clipper.logger.logEvent(new Log.Event.BaseEvent(Log.Event.Label.SetDoNotPromptRatings)); - } - - /** - * Set ClipperStorageKeys.isRatingsPromptLogicExecutedInEdge value to "true" - */ - public static setIsRatingsPromptLogicExecutedInEdge(): void { - Clipper.storeValue(ClipperStorageKeys.isRatingsPromptLogicExecutedInEdge, "true"); - - Clipper.logger.logEvent(new Log.Event.BaseEvent(Log.Event.Label.SetIsRatingsPromptLogicExecutedInEdge)); - } - - /** - * We will show the ratings prompt if ALL of the below applies: - * * Ratings prompt is enabled for the ClientType/ClipperType - * * If ClipperStorageKeys.doNotPromptRatings is not "true" - * * If RatingsHelper.badRatingTimingDelayIsOver(...) returns true when provided ClipperStorageKeys.lastBadRatingDate - * * If RatingsHelper.badRatingVersionDelayIsOver(...) returns true when provided ClipperStorageKeys.lastBadRatingVersion and ClipperStorageKeys.lastSeenVersion - * * If RatingsHelper.clipSuccessDelayIsOver(...) returns true when provided ClipperStorageKeys.numClipSuccess - */ - public static shouldShowRatingsPrompt(clipperState: ClipperState): boolean { - let shouldShowRatingsPromptEvent = new Log.Event.PromiseEvent(Log.Event.Label.ShouldShowRatingsPrompt); - let shouldShowRatingsPromptInfo: RatingsLoggingInfo = {}; - - let shouldShowRatingsPrompt: boolean = RatingsHelper.shouldShowRatingsPromptInternal(clipperState, shouldShowRatingsPromptEvent, shouldShowRatingsPromptInfo); - - shouldShowRatingsPromptEvent.setCustomProperty(Log.PropertyName.Custom.ShouldShowRatingsPrompt, shouldShowRatingsPrompt); - shouldShowRatingsPromptEvent.setCustomProperty(Log.PropertyName.Custom.RatingsInfo, JSON.stringify(shouldShowRatingsPromptInfo)); - Clipper.logger.logEvent(shouldShowRatingsPromptEvent); - - return shouldShowRatingsPrompt; - } - - /** - * Returns true if the ratings prompt is enabled for ClientType/ClipperType provided - * - * Public for testing - */ - public static ratingsPromptEnabledForClient(clientType: ClientType): boolean { - let settingName: string = RatingsHelper.getRatingsPromptEnabledSettingNameForClient(clientType); - let isEnabledAsStr: string = Settings.getSetting(settingName); - return !ObjectUtils.isNullOrUndefined(isEnabledAsStr) && isEnabledAsStr.toLowerCase() === "true"; - } - - /** - * Returns true if ONE of the below applies: - * 1) A bad rating has never been given by the user, OR - * 2) Bad rating date provided is valid and occurred more than {Constants.Settings.minTimeBetweenBadRatings} ago - * from the valid comparison date (e.g., the current date) provided - * - * Public for testing - */ - public static badRatingTimingDelayIsOver(badRatingDate: number, comparisonDate: number): boolean { - if (isNaN(badRatingDate)) { - // value has never been set, no bad rating given - return true; - } - - if (!RatingsHelper.isValidDate(badRatingDate) || !RatingsHelper.isValidDate(comparisonDate)) { - return false; - } - - return (comparisonDate - badRatingDate) >= Constants.Settings.minTimeBetweenBadRatings; - } - - /** - * Returns true if ONE of the below applies: - * 1) A bad rating has never been given by the user, OR - * 2) There has been a non-patch version update since the bad rating, i.e., - * a) The major version last seen by the user is greater than the major version at the time of the last bad rating, OR - * b) The major version remained the same, while the minor version last seen by the user is greater than - * the minor version at the time of the last bad rating - * - * Public for testing - */ - public static badRatingVersionDelayIsOver(badRatingVersionAsStr: string, lastSeenVersionAsStr: string): boolean { - if (ObjectUtils.isNullOrUndefined(badRatingVersionAsStr)) { - // value has never been set, no bad rating given - return true; - } - - let badRatingVersion: Version; - let lastSeenVersion: Version; - try { - badRatingVersion = new Version(badRatingVersionAsStr); - lastSeenVersion = new Version(lastSeenVersionAsStr); - } catch (e) { - return false; - } - - return lastSeenVersion.isGreaterThan(badRatingVersion, true /* ignorePatchUpdate */); - } - - /** - * Returns true if ALL of the below applies: - * * (Number of successful clips - Anchor clip value) >= {Constants.Settings.minClipSuccessForRatingsPrompt} - * * (Number of successful clips - Anchor clip value) <= {Constants.Settings.maxClipSuccessForRatingsPrompt} - * - * Public for testing - */ - public static clipSuccessDelayIsOver(numClips: number, anchorClipValue?: number): boolean { - if (isNaN(numClips)) { - return false; - } - - if (isNaN(anchorClipValue)) { - anchorClipValue = 0; - } - - let numClipsAdjusted = numClips - anchorClipValue; - - return numClipsAdjusted >= Constants.Settings.minClipSuccessForRatingsPrompt && - numClipsAdjusted <= Constants.Settings.maxClipSuccessForRatingsPrompt; - } - - /** - * Sets ClipperStorageKeys.numSuccessfulClipsRatingsEnablement to be - * the current value of (ClipperStorageKeys.numSuccessfulClips - 1), if needed. - * - * The set is "needed" if ALL of the below applies: - * * The user has not already interacted with the prompt (ClipperStorageKeys.doNotPromptRatings is not set) - * * The ratings prompt logic has not already executed in Edge or - * ClipperStorageKeys.numSuccessfulClipsRatingsEnablement has not already been set - * - * Public for testing - * - * NOTE OF EXPLANATION: We first check if the user has already interacted with the prompt for backwards compatibility - * with the original implementation of the ratings prompt that did not include this method. It ensures that we will not - * re-raise the prompt for users who have already interacted with it (although it is possible users who didn't interact - * with the original prompt see it up to twice as many times as originally planned). - */ - public static setNumSuccessfulClipsRatingsEnablement(clientType: ClientType): void { - let doNotPromptRatingsAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.doNotPromptRatings); - if (RatingsHelper.doNotPromptRatingsIsSet(doNotPromptRatingsAsStr)) { - return; - } - - /** - * If the ratings prompt logic hasn't been executed in Edge, then we would - * need numSuccessfulClipsRatingsEnablement to be updated even if it has - * already been set. This is because the user hasn't already interacted - * with the prompt, and we need to ensure that the difference between the - * number of successful clips and the anchor clip value is less than or - * equal to the maximum number of successful clips for ratings enablement, - * as per the logic in clipSuccessDelayIsOver(...). - */ - let isRatingsPromptLogicExecutedInEdgeAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.isRatingsPromptLogicExecutedInEdge); - if (clientType !== ClientType.EdgeExtension || RatingsHelper.isRatingsPromptLogicExecutedInEdge(isRatingsPromptLogicExecutedInEdgeAsStr)) { - let numSuccessfulClipsRatingsEnablementAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.numSuccessfulClipsRatingsEnablement); - if (parseInt(numSuccessfulClipsRatingsEnablementAsStr, 10) >= 0) { - return; - } - } else if (clientType === ClientType.EdgeExtension) { - RatingsHelper.setIsRatingsPromptLogicExecutedInEdge(); - } - - let numSuccessfulClips: number = parseInt(Clipper.getCachedValue(ClipperStorageKeys.numSuccessfulClips), 10); - // subtracting 1 below to account for the fact that this set is occuring after one already successful clip - Clipper.storeValue(ClipperStorageKeys.numSuccessfulClipsRatingsEnablement, (numSuccessfulClips - 1).toString()); - } - - /** - * Implementation of the logic described in the setShowRatingsPromptState(...) description - */ - private static shouldShowRatingsPromptInternal(clipperState: ClipperState, event: Log.Event.PromiseEvent, logEventInfo: RatingsLoggingInfo): boolean { - if (ObjectUtils.isNullOrUndefined(clipperState)) { - event.setStatus(Log.Status.Failed); - event.setFailureInfo({ error: "Clipper state is null or undefined" }); - return false; - } - - if (!ObjectUtils.isNullOrUndefined(clipperState.showRatingsPrompt)) { - // Return cached value in clipper state since it already exists - logEventInfo.usedCachedValue = true; - return clipperState.showRatingsPrompt; - } - - let ratingsPromptEnabled: boolean = RatingsHelper.ratingsPromptEnabledForClient(clipperState.clientInfo.clipperType); - logEventInfo.ratingsPromptEnabledForClient = ratingsPromptEnabled; - if (!ratingsPromptEnabled) { - return false; - } - - RatingsHelper.setNumSuccessfulClipsRatingsEnablement(clipperState.clientInfo.clipperType); - - let doNotPromptRatingsStr: string = Clipper.getCachedValue(ClipperStorageKeys.doNotPromptRatings); - let lastBadRatingDateAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.lastBadRatingDate); - let lastBadRatingVersion: string = Clipper.getCachedValue(ClipperStorageKeys.lastBadRatingVersion); - let lastSeenVersion: string = Clipper.getCachedValue(ClipperStorageKeys.lastSeenVersion); - let numClipsAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.numSuccessfulClips); - let numClipsAnchorAsStr: string = Clipper.getCachedValue(ClipperStorageKeys.numSuccessfulClipsRatingsEnablement); - - if (RatingsHelper.doNotPromptRatingsIsSet(doNotPromptRatingsStr)) { - logEventInfo.doNotPromptRatings = true; - return false; - } - - let lastBadRatingDate: number = parseInt(lastBadRatingDateAsStr, 10); - let numClips: number = parseInt(numClipsAsStr, 10); - let numClipsAnchor: number = parseInt(numClipsAnchorAsStr, 10); - - /* tslint:disable:no-null-keyword */ - // null is the value storage gives back; also, setting to undefined will keep this kvp from being logged at all - logEventInfo.lastBadRatingDate = lastBadRatingDate ? new Date(lastBadRatingDate).toString() : null; - /* tslint:enable:no-null-keyword */ - logEventInfo.lastBadRatingVersion = lastBadRatingVersion; - logEventInfo.lastSeenVersion = lastSeenVersion; - logEventInfo.numSuccessfulClips = numClips; - logEventInfo.numSuccessfulClipsAnchor = numClipsAnchor; - - let badRatingTimingDelayIsOver: boolean = RatingsHelper.badRatingTimingDelayIsOver(lastBadRatingDate, Date.now()); - let badRatingVersionDelayIsOver: boolean = RatingsHelper.badRatingVersionDelayIsOver(lastBadRatingVersion, lastSeenVersion); - let clipSuccessDelayIsOver: boolean = RatingsHelper.clipSuccessDelayIsOver(numClips, numClipsAnchor); - - logEventInfo.badRatingTimingDelayIsOver = badRatingTimingDelayIsOver; - logEventInfo.badRatingVersionDelayIsOver = badRatingVersionDelayIsOver; - logEventInfo.clipSuccessDelayIsOver = clipSuccessDelayIsOver; - - if (badRatingTimingDelayIsOver && badRatingVersionDelayIsOver && clipSuccessDelayIsOver) { - return true; - } - - return false; - } - - private static combineClientTypeAndSuffix(clientType: ClientType, suffix: string): string { - return ClientType[clientType] + suffix; - } - - private static getRateUrlSettingNameForClient(clientType: ClientType): string { - return RatingsHelper.combineClientTypeAndSuffix(clientType, RatingsHelper.rateUrlSettingNameSuffix); - } - - private static getRatingsPromptEnabledSettingNameForClient(clientType: ClientType): string { - return RatingsHelper.combineClientTypeAndSuffix(clientType, RatingsHelper.ratingsPromptEnabledSettingNameSuffix); - } - - private static isValidDate(date: number): boolean { - let minimumTimeValue: number = (Constants.Settings.maximumJSTimeValue * -1); - return date >= minimumTimeValue && date <= Constants.Settings.maximumJSTimeValue; - } - - private static doNotPromptRatingsIsSet(doNotPromptRatingsStr: string): boolean { - return !ObjectUtils.isNullOrUndefined(doNotPromptRatingsStr) && doNotPromptRatingsStr.toLowerCase() === "true"; - } - - private static isRatingsPromptLogicExecutedInEdge(isRatingsPromptLogicExecutedInEdgeAsStr: string): boolean { - return !ObjectUtils.isNullOrUndefined(isRatingsPromptLogicExecutedInEdgeAsStr) && isRatingsPromptLogicExecutedInEdgeAsStr.toLowerCase() === "true"; - } -} diff --git a/src/scripts/clipperUI/regionSelector.tsx b/src/scripts/clipperUI/regionSelector.tsx deleted file mode 100644 index fe82d154..00000000 --- a/src/scripts/clipperUI/regionSelector.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import {DomUtils} from "../domParsers/domUtils"; - -import * as Log from "../logging/log"; - -import {Constants} from "../constants"; -import {ClientType} from "../clientType"; -import {Status} from "./status"; - -import {ClipperStateProp} from "./clipperState"; -import {ComponentBase} from "./componentBase"; -import {Clipper} from "./frontEndGlobals"; -import {ExtensionUtils} from "../extensions/extensionUtils"; -import {Localization} from "../localization/localization"; - -export interface Point { - x: number; - y: number; -}; - -interface RegionSelectorState { - firstPoint?: Point; - secondPoint?: Point; - mousePosition?: Point; - selectionInProgress?: boolean; - keyboardSelectionInProgress?: boolean; - winWidth?: number; - winHeight?: number; - ariaLiveMessage?: string; - ariaAlertMessage?: string; -} - -class RegionSelectorClass extends ComponentBase { - private devicePixelRatio: number = 1; - private cursorSpeed: number = 1; - - private resizeHandler = this.handleResize.bind(this); - private mouseMovementHandler = this.globalMouseMoveHandler.bind(this); - private mouseOverHandler = this.globalMouseOverHandler.bind(this); - private keyDownDict: { [key: number]: boolean } = {}; - - getInitialState(): RegionSelectorState { - return { - selectionInProgress: false, - keyboardSelectionInProgress: false, - winHeight: window.innerHeight, - winWidth: window.innerWidth, - mousePosition: {x: window.innerWidth / 2, y: window.innerHeight / 2} - }; - } - - constructor(props: ClipperStateProp) { - super(props); - this.resetState(); - - window.addEventListener("resize", this.resizeHandler); - window.addEventListener("mousemove", this.mouseMovementHandler); - window.addEventListener("mouseover", this.mouseOverHandler); - } - - private onunload() { - window.removeEventListener("resize", this.resizeHandler); - window.removeEventListener("mousemove", this.mouseMovementHandler); - window.removeEventListener("mouseover", this.mouseOverHandler); - } - - /** - * Start the selection process over - */ - private resetState() { - this.setState({ firstPoint: undefined, secondPoint: undefined, selectionInProgress: false, keyboardSelectionInProgress: false}); - this.props.clipperState.setState({ regionResult: { status: Status.NotStarted, data: this.props.clipperState.regionResult.data } }); - } - - /** - * Define the starting point for the selection - */ - private startSelection(point: Point, fromKeyboard = false) { - if (this.props.clipperState.regionResult.status !== Status.InProgress) { - this.setState({ firstPoint: point, secondPoint: undefined, selectionInProgress: true, keyboardSelectionInProgress: fromKeyboard }); - this.props.clipperState.setState({ regionResult: { status: Status.InProgress, data: this.props.clipperState.regionResult.data } }); - } - } - - /** - * The selection is complete - */ - private completeSelection(dataUrl: string) { - let regionList = this.props.clipperState.regionResult.data; - if (!regionList) { - regionList = []; - } - regionList.push(dataUrl); - this.props.clipperState.setState({ regionResult: { status: Status.Succeeded, data: regionList } }); - } - - /** - * They are selecting, update the second point - */ - private moveSelection(point: Point) { - this.setState({ secondPoint: point }); - } - - /** - * Update Mouse Position for custom cursor - */ - private setMousePosition(point: Point) { - this.setState({ mousePosition: point}); - } - - /** - * Get the direction message for screen reader based on key presses - */ - private getDirectionMessage(): string { - let directions: string[] = []; - - if (this.keyDownDict[Constants.KeyCodes.up]) { - directions.push(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.Up")); - } - if (this.keyDownDict[Constants.KeyCodes.down]) { - directions.push(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.Down")); - } - if (this.keyDownDict[Constants.KeyCodes.left]) { - directions.push(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.Left")); - } - if (this.keyDownDict[Constants.KeyCodes.right]) { - directions.push(Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.Right")); - } - - return directions.join(" "); - } - - /** - * Define the ending point, and notify the main UI - */ - private stopSelection(point: Point) { - if (this.state.selectionInProgress) { - if (!this.state.firstPoint || !this.state.secondPoint || this.state.firstPoint.x === this.state.secondPoint.x || this.state.firstPoint.y === this.state.secondPoint.y) { - // Nothing to clip, start over - this.resetState(); - } else { - this.setState({ secondPoint: point, selectionInProgress: false, keyboardSelectionInProgress: false }); - // Get the image immediately - this.startRegionClip(); - } - } - } - - private mouseDownHandler(e: MouseEvent) { - // Prevent default "dragging" which sometimes occurs - e.preventDefault(); - this.startSelection({ x: e.pageX, y: e.pageY }); - } - - private keyDownHandler(e: KeyboardEvent) { - this.keyDownDict[e.which] = true; - - if (e.which === Constants.KeyCodes.enter ) { - if (!this.state.selectionInProgress) { - this.startSelection({ x: this.state.mousePosition.x, y: this.state.mousePosition.y }, true /* fromKeyboard */); - this.setState({ ariaLiveMessage: Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.SelectionStarted") }); - } else { - this.setState({ ariaAlertMessage: Localization.getLocalizedString("WebClipper.Accessibility.ScreenReader.SelectionComplete") }); - this.stopSelection({ x: this.state.mousePosition.x, y: this.state.mousePosition.y }); - } - e.preventDefault(); - } else if (e.which === Constants.KeyCodes.up - || e.which === Constants.KeyCodes.down - || e.which === Constants.KeyCodes.left - || e.which === Constants.KeyCodes.right) { - - let delta: Point = {x: 0, y: 0}; - - if (this.keyDownDict[Constants.KeyCodes.up]) { - delta.y -= this.cursorSpeed; - } - - if (this.keyDownDict[Constants.KeyCodes.down]) { - delta.y += this.cursorSpeed; - } - - if (this.keyDownDict[Constants.KeyCodes.left]) { - delta.x -= this.cursorSpeed; - } - - if (this.keyDownDict[Constants.KeyCodes.right]) { - delta.x += this.cursorSpeed; - } - - let newPosition: Point = {x: Math.max(Math.min(this.state.mousePosition.x + delta.x, this.state.winWidth), 0), y: Math.max(Math.min(this.state.mousePosition.y + delta.y, this.state.winHeight), 0)}; - - this.setMousePosition(newPosition); - - if (!e.repeat) { - let directionMessage = this.getDirectionMessage(); - if (directionMessage) { - this.setState({ ariaLiveMessage: directionMessage }); - } - } - - if (this.state.selectionInProgress) { - this.moveSelection(newPosition); - } - - if (this.cursorSpeed < 5) { - this.cursorSpeed++; - } - - e.preventDefault(); - } - } - - private keyUpHandler(e: KeyboardEvent) { - this.keyDownDict[e.which] = false; - if (e.which === Constants.KeyCodes.up - || e.which === Constants.KeyCodes.down - || e.which === Constants.KeyCodes.left - || e.which === Constants.KeyCodes.right) { - this.cursorSpeed = 1; - } - } - - private globalMouseMoveHandler(e: MouseEvent) { - this.setMousePosition({ x: e.pageX, y: e.pageY }); - } - - private globalMouseOverHandler(e: MouseEvent) { - window.removeEventListener("mouseover", this.mouseOverHandler); - this.setMousePosition({ x: e.pageX, y: e.pageY }); - } - - private mouseMoveHandler(e: MouseEvent) { - if (this.state.selectionInProgress) { - if (e.buttons === 0 && !this.state.keyboardSelectionInProgress) { - // They let go of the mouse while outside the window, stop the selection where they went out - this.stopSelection(this.state.secondPoint); - return; - } - - this.moveSelection({ x: e.pageX, y: e.pageY }); - } - } - - private mouseUpHandler(e: MouseEvent) { - this.stopSelection({ x: e.pageX, y: e.pageY }); - } - - private touchStartHandler(e: TouchEvent) { - let eventPoint = e.changedTouches[0]; - this.startSelection({ x: eventPoint.clientX, y: eventPoint.clientY }); - } - - private touchMoveHandler(e: TouchEvent) { - if (this.state.selectionInProgress) { - let eventPoint = e.changedTouches[0]; - this.moveSelection({ x: eventPoint.clientX, y: eventPoint.clientY }); - e.preventDefault(); - } - } - - private touchEndHandler(e: TouchEvent) { - let eventPoint = e.changedTouches[0]; - this.stopSelection({ x: eventPoint.clientX, y: eventPoint.clientY }); - } - - private handleResize() { - this.setState({ winHeight: window.innerHeight, winWidth: window.innerWidth }); - } - - /** - * Update all of the frames and elements according to the selection - */ - private updateVisualElements(element: HTMLElement, isInitialized: boolean) { - let outerFrame: HTMLCanvasElement = this.refs.outerFrame as HTMLCanvasElement; - if (!outerFrame) { - return; - } - - let cursor: HTMLImageElement = this.refs.cursor as HTMLImageElement; - if (cursor) { - cursor.style.left = this.state.mousePosition.x + "px"; - cursor.style.top = this.state.mousePosition.y + "px"; - } - - let xMin: number; - let yMin: number; - let xMax: number; - let yMax: number; - - if (!this.state.firstPoint || !this.state.secondPoint) { - xMin = 0; - yMin = 0; - xMax = 0; - yMax = 0; - } else { - xMin = Math.min(this.state.firstPoint.x, this.state.secondPoint.x); - yMin = Math.min(this.state.firstPoint.y, this.state.secondPoint.y); - xMax = Math.max(this.state.firstPoint.x, this.state.secondPoint.x); - yMax = Math.max(this.state.firstPoint.y, this.state.secondPoint.y); - - let innerFrame: HTMLCanvasElement = this.refs.innerFrame as HTMLCanvasElement; - if (innerFrame) { - // We don't worry about -1 values as they simply go offscreen neatly - let borderWidth = 1; - innerFrame.style.top = yMin - borderWidth + "px"; - innerFrame.style.left = xMin - borderWidth + "px"; - innerFrame.style.height = yMax - yMin + "px"; - innerFrame.style.width = xMax - xMin + "px"; - } - } - - let winWidth = this.state.winWidth; - let winHeight = this.state.winHeight; - - let context = outerFrame.getContext("2d"); - context.canvas.width = winWidth; - context.canvas.height = winHeight; - - context.beginPath(); - context.fillStyle = "black"; - context.fillRect(0, 0, xMin, winHeight); - context.fillRect(xMin, 0, xMax - xMin, yMin); - context.fillRect(xMax, 0, winWidth - xMax, winHeight); - context.fillRect(xMin, yMax, xMax - xMin, winHeight - yMax); - - if (!isInitialized) { - element.focus(); - } - } - - /** - * Get the browser to capture a screenshot, and save off the portion they selected (which may be compressed until it's below - * the maximum allowed size) - */ - private startRegionClip() { - // Taken from https://www.kirupa.com/html5/detecting_retina_high_dpi.htm - // We check this here so that we can log it as a custom property on the regionSelectionProcessingEvent - const query = "(-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2), (min-resolution: 192dpi)"; - const isHighDpiScreen = matchMedia(query).matches; - const isFirefoxWithHighDpiDisplay = this.props.clipperState.clientInfo.clipperType === ClientType.FirefoxExtension && isHighDpiScreen; - - // Firefox reports this value incorrectly if this iframe is hidden, so store it now since we know we're visible - // In addition to this, Firefox currently has a bug where they are not using devicePixelRatio correctly - // on HighDPI screens such as Retina screens or the Surface Pro 4 - // Bug link: https://bugzilla.mozilla.org/show_bug.cgi?id=1278507 - this.devicePixelRatio = isFirefoxWithHighDpiDisplay ? window.devicePixelRatio / 2 : window.devicePixelRatio; - - let regionSelectionProcessingEvent = new Log.Event.BaseEvent(Log.Event.Label.RegionSelectionProcessing); - let regionSelectionCapturingEvent = new Log.Event.BaseEvent(Log.Event.Label.RegionSelectionCapturing); - regionSelectionCapturingEvent.setCustomProperty(Log.PropertyName.Custom.Width, Math.abs(this.state.firstPoint.x - this.state.secondPoint.x)); - regionSelectionCapturingEvent.setCustomProperty(Log.PropertyName.Custom.Height, Math.abs(this.state.firstPoint.y - this.state.secondPoint.y)); - - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.takeTabScreenshot, { - callback: (dataUrl: string) => { - Clipper.logger.logEvent(regionSelectionCapturingEvent); - this.saveCompressedSelectionToState(dataUrl).then((canvas) => { - regionSelectionProcessingEvent.setCustomProperty(Log.PropertyName.Custom.Width, canvas.width); - regionSelectionProcessingEvent.setCustomProperty(Log.PropertyName.Custom.Height, canvas.height); - regionSelectionProcessingEvent.setCustomProperty(Log.PropertyName.Custom.IsHighDpiScreen, isHighDpiScreen); - Clipper.logger.logEvent(regionSelectionProcessingEvent); - }); - } - }); - } - - /** - * Given a base image in url form, captures the sub-image defined by the state's first and second points, compresses it if - * necessary, then saves it to state if the process was successful - */ - private saveCompressedSelectionToState(baseDataUrl: string): Promise { - return this.createSelectionAsCanvas(baseDataUrl).then((canvas) => { - let compressedSelection = this.getCompressedDataUrl(canvas); - this.completeSelection(compressedSelection); - return Promise.resolve(canvas); - }).catch((error: Error) => { - Clipper.logger.logFailure(Log.Failure.Label.RegionSelectionProcessing, Log.Failure.Type.Unexpected, - { error: error.message }); - this.resetState(); - return Promise.reject(error); - }); - } - - /** - * Given a base image in url form, creates a canvas containing the sub-image defined by the state's first and second points - */ - private createSelectionAsCanvas(baseDataUrl: string): Promise { - if (!baseDataUrl) { - return Promise.reject(new Error("baseDataUrl should be a non-empty string, but was: " + baseDataUrl)); - } - - if (!this.state.firstPoint || !this.state.secondPoint) { - return Promise.reject(new Error("Expected the two points to be set, but they were not")); - } - - const devicePixelRatio = this.devicePixelRatio; - - return new Promise((resolve) => { - let regionSelectionLoadingEvent = new Log.Event.BaseEvent(Log.Event.Label.RegionSelectionLoading); - let img: HTMLImageElement = new Image(); - - img.onload = () => { - Clipper.logger.logEvent(regionSelectionLoadingEvent); - - let xMin = Math.min(this.state.firstPoint.x, this.state.secondPoint.x); - let yMin = Math.min(this.state.firstPoint.y, this.state.secondPoint.y); - let xMax = Math.min(img.width, Math.max(this.state.firstPoint.x, this.state.secondPoint.x)); - let yMax = Math.min(img.height, Math.max(this.state.firstPoint.y, this.state.secondPoint.y)); - - let destinationOffsetX = 0; - let destinationOffsetY = 0; - let width = (xMax - xMin); - let height = (yMax - yMin); - let sourceOffsetX = xMin * devicePixelRatio; - let sourceOffsetY = yMin * devicePixelRatio; - let sourceWidth = (xMax - xMin) * devicePixelRatio; - let sourceHeight = (yMax - yMin) * devicePixelRatio; - - let canvas: HTMLCanvasElement = document.createElement("canvas") as HTMLCanvasElement; - canvas.width = width; - canvas.height = height; - let ctx: CanvasRenderingContext2D = canvas.getContext("2d"); - ctx.drawImage(img, sourceOffsetX, sourceOffsetY, sourceWidth, sourceHeight, destinationOffsetX, destinationOffsetY, width, height); - resolve(canvas); - }; - - img.src = baseDataUrl; - }); - } - - /* - * Converts the canvas to a Base64 encoded URI, compressing it by lowering its quality if above the maxBytes threshold - */ - private getCompressedDataUrl(node: Node): string { - let compressEvent = new Log.Event.BaseEvent(Log.Event.Label.CompressRegionSelection); - - let canvas: HTMLCanvasElement = node as HTMLCanvasElement; - - // First, see if the best quality PNG will work. - let dataUrl: string = canvas.toDataURL("image/png"); - compressEvent.setCustomProperty(Log.PropertyName.Custom.InitialDataUrlLength, dataUrl.length); - - dataUrl = DomUtils.adjustImageQualityIfNecessary(canvas, dataUrl); - - compressEvent.setCustomProperty(Log.PropertyName.Custom.FinalDataUrlLength, dataUrl.length); - Clipper.logger.logEvent(compressEvent); - - return dataUrl; - } - - private getInnerFrame() { - if (this.state.secondPoint) { - return
    ; - } - - return undefined; - } - - render() { - let innerFrameElement = this.getInnerFrame(); - - return ( -
    -
    - {this.state.ariaLiveMessage} -
    -
    - {this.state.ariaAlertMessage} -
    - - - {innerFrameElement} -
    - ); - } -} - -let component = RegionSelectorClass.componentize(); -export {component as RegionSelector}; diff --git a/src/scripts/clipperUI/tooltip.tsx b/src/scripts/clipperUI/tooltip.tsx deleted file mode 100644 index 8fb25bc7..00000000 --- a/src/scripts/clipperUI/tooltip.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {Constants} from "../constants"; - -import {Localization} from "../localization/localization"; - -import {ComponentBase} from "./componentBase"; - -import {CloseButton} from "./components/closeButton"; - -export interface TooltipProps { - elementId: string; - title?: string; - onCloseButtonHandler: () => void; - onElementDraw?: (el: HTMLElement) => void; - renderablePanel: any; - contentClasses?: string[]; - brandingImage?: any; -} - -/** - * Renders content with consistent tooltip styling - */ -export class TooltipClass extends ComponentBase<{}, TooltipProps> { - getInitialState(): {} { - return {}; - } - - render() { - let additionalContentClasses = ""; - if (this.props.contentClasses) { - additionalContentClasses = this.props.contentClasses.join(" "); - } - return ( -
    - {this.props.brandingImage} - {this.props.title ?
    {this.props.title}
    : undefined} - -
    - {this.props.renderablePanel} -
    -
    - ); - } -} - -let component = TooltipClass.componentize(); -export {component as Tooltip} diff --git a/src/scripts/clipperUI/tooltipRenderer.tsx b/src/scripts/clipperUI/tooltipRenderer.tsx deleted file mode 100644 index d4a2f8b4..00000000 --- a/src/scripts/clipperUI/tooltipRenderer.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import {Constants} from "../constants"; - -import {ExtensionUtils} from "../extensions/extensionUtils"; -import {InvokeSource} from "../extensions/invokeSource"; - -import {Localization} from "../localization/localization"; - -import {Clipper} from "./frontEndGlobals"; -import {ComponentBase} from "./componentBase"; -import {AnimatedTooltip} from "./animatedTooltip"; -import {TooltipProps} from "./tooltipProps"; -import {TooltipType, TooltipTypeUtils} from "./tooltipType"; - -import {NewHeightInfo} from "./animations/slidingHeightAnimationStrategy"; - -import {ChangeLogPanel} from "./panels/changeLogPanel"; -import {DialogButton, DialogPanel} from "./panels/dialogPanel"; - -export interface TooltipRendererState { - tooltipToRenderOverride?: TooltipType; -} - -export interface TooltipRendererProps { - onCloseButtonHandler?: () => void; - onHeightChange?: (newHeightInfo: NewHeightInfo) => void; - onTooltipClose?: () => void; - tooltipToRender?: TooltipType; - tooltipProps?: any; -} - -class TooltipRendererClass extends ComponentBase { - getInitialState(): TooltipRendererState { - return {}; - } - - getChangeLogPanel() { - let whatsNewProps = this.props.tooltipProps as TooltipProps.WhatsNew; - let handleProceedToWebClipperButton = () => { - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.invokeClipperFromPageNav, { - param: InvokeSource.WhatsNewTooltip - }); - }; - - return ( -
    - -
    - - - {Localization.getLocalizedString("WebClipper.Label.ProceedToWebClipperFun") } - - -
    -
    - ); - } - - getContentClasses(): {} { - let currentPanel = this.getCurrentPanelType(); - let classes: string[] = []; - switch (currentPanel) { - case TooltipType.Pdf: - /* falls through */ - case TooltipType.Product: - /* falls through */ - case TooltipType.Recipe: - /* falls through */ - case TooltipType.Video: - classes.push("tooltip-upsell"); - return classes; - case TooltipType.ChangeLog: - classes.push("changelog-content"); - return classes; - default: - return classes; - } - } - - getCurrentPanelType(): TooltipType { - return this.state.hasOwnProperty("tooltipToRenderOverride") ? this.state.tooltipToRenderOverride : this.props.tooltipToRender; - } - - getCurrentTitle(): string { - let currentPanel = this.getCurrentPanelType(); - switch (currentPanel) { - case TooltipType.ChangeLog: - return Localization.getLocalizedString("WebClipper.Label.WhatsNew"); - default: - return ""; - } - } - - getTooltipPanel(tooltipType: TooltipType) { - let handleProceedToWebClipperButton = () => { - Clipper.getExtensionCommunicator().callRemoteFunction(Constants.FunctionKeys.invokeClipperFromPageNav, { - param: TooltipTypeUtils.toInvokeSource(tooltipType) - }); - }; - - let tooltipAsString = TooltipType[tooltipType]; - let tooltipImagePath = "tooltips/" + tooltipAsString + ".png"; - tooltipImagePath = tooltipImagePath.toLowerCase(); - - let content: HTMLElement[] = [()]; - - let buttons: DialogButton[] = [{ - id: Constants.Ids.proceedToWebClipperButton, - label: Localization.getLocalizedString("WebClipper.Label.ProceedToWebClipperFun"), - handler: handleProceedToWebClipperButton - }]; - - let message = "WebClipper.Label." + tooltipAsString + "Tooltip"; - return ; - } - - getWhatsNewPanel() { - let onShowChangeLogButton = () => { - this.setState({ tooltipToRenderOverride: TooltipType.ChangeLog }); - }; - let buttons: DialogButton[] = [{ - id: Constants.Ids.checkOutWhatsNewButton, - label: Localization.getLocalizedString("WebClipper.Label.OpenChangeLogFromTooltip"), - handler: onShowChangeLogButton.bind(this) - }]; - return ; - } - - createTooltipPanelToShow(): any { - let currentPanel = this.getCurrentPanelType(); - - if (currentPanel === undefined) { - return undefined; - } - - switch (currentPanel) { - case TooltipType.WhatsNew: - return this.getWhatsNewPanel(); - case TooltipType.ChangeLog: - return this.getChangeLogPanel(); - default: - return this.getTooltipPanel(currentPanel); - } - } - - getBrandingImage(): any { - let currentPanel = this.getCurrentPanelType(); - switch (currentPanel) { - case TooltipType.ChangeLog: - return undefined; - default: - return ( -
    -

    - -

    -
    - ); - } - } - - render() { - return ( - - ); - } -} - -let component = TooltipRendererClass.componentize(); -export {component as TooltipRenderer} diff --git a/src/scripts/clipperUI/unsupportedBrowser.ts b/src/scripts/clipperUI/unsupportedBrowser.ts deleted file mode 100644 index ad3f2cb1..00000000 --- a/src/scripts/clipperUI/unsupportedBrowser.ts +++ /dev/null @@ -1,120 +0,0 @@ -import {Constants} from "../constants"; -import {Polyfills} from "../polyfills"; - -import {ExtensionUtils} from "../extensions/extensionUtils"; - -import {Localization} from "../localization/localization"; -import {LocalizationHelper} from "../localization/localizationHelper"; - -import {Status} from "./status"; - -import {LoadingPanel} from "./panels/loadingPanel"; - -export interface UnsupportedBrowserState { - localizedStringFetchAttemptCompleted: Status; -} - -/** - * A very browser-compliant class that notifies the user that their browser is unsupported - * by the Web Clipper. - */ -class UnsupportedBrowserClass { - public state: UnsupportedBrowserState; - - constructor() { - this.state = this.getInitialState(); - } - - getInitialState(): UnsupportedBrowserState { - return { - localizedStringFetchAttemptCompleted: Status.NotStarted - }; - } - - public setState(newPartialState: UnsupportedBrowserState) { - m.startComputation(); - for (let key in newPartialState) { - if (newPartialState.hasOwnProperty(key)) { - this.state[key] = newPartialState[key]; - } - } - m.endComputation(); - } - - public static componentize() { - let returnValue: any = () => { }; - returnValue.controller = (props: any) => { - return new (this as any)(props); - }; - returnValue.view = (controller: any, props: any) => { - controller.props = props; - return controller.render(); - }; - - return returnValue; - } - - private fetchLocalizedStrings(locale: string) { - this.setState({ - localizedStringFetchAttemptCompleted: Status.InProgress - }); - LocalizationHelper.makeLocStringsFetchRequest(locale).then((responsePackage) => { - try { - Localization.setLocalizedStrings(JSON.parse(responsePackage.parsedResponse)); - this.setState({ - localizedStringFetchAttemptCompleted: Status.Succeeded - }); - } catch (e) { - this.setState({ - localizedStringFetchAttemptCompleted: Status.Failed - }); - } - }).catch(() => { - this.setState({ - localizedStringFetchAttemptCompleted: Status.Failed - }); - }); - } - - private attemptingFetchLocalizedStrings() { - return this.state.localizedStringFetchAttemptCompleted === Status.NotStarted || - this.state.localizedStringFetchAttemptCompleted === Status.InProgress; - } - - render() { - if (this.state.localizedStringFetchAttemptCompleted === Status.NotStarted) { - // navigator.userLanguage is only available in IE, and Typescript will not recognize this property - this.fetchLocalizedStrings(navigator.language || (navigator).userLanguage); - } - - // In IE8 and below, 'class' is a reserved keyword and cannot be used as a key in a JSON object - return ({tag: "div", attrs: {id: Constants.Ids.unsupportedBrowserContainer}, children: [ - {tag: "div", attrs: {id: Constants.Ids.unsupportedBrowserPanel, "class": "panelContent"}, children: [ - {tag: "div", attrs: {className: Constants.Classes.heightAnimator, style: "min-height: 276px; max-height: 276px;"}, children: [ - {tag: "div", attrs: {className: Constants.Classes.panelAnimator, style: "left: 0px; opacity: 1;"}, children: [ - {tag: "div", attrs: {id: Constants.Ids.signInContainer}, children: [ - {tag: "div", attrs: {className: "signInPadding"}, children: [ - {tag: "img", attrs: {id: Constants.Ids.signInLogo, src: ExtensionUtils.getImageResourceUrl("onenote_logo_clipper.png")}}, - {tag: "div", attrs: {id: Constants.Ids.signInMessageLabelContainer, "class": "messageLabelContainer"}, children: [ - {tag: "span", attrs: {"class": "messageLabel", style: Localization.getFontFamilyAsStyle(Localization.FontFamily.Regular)}, children: [ - this.attemptingFetchLocalizedStrings() ? "" : Localization.getLocalizedString("WebClipper.Label.OneNoteClipper") - ]} - ]}, - {tag: "div", attrs: {"class": "signInDescription"}, children: [ - {tag: "span", attrs: {id: Constants.Ids.signInText, style: Localization.getFontFamilyAsStyle(Localization.FontFamily.Light)}, children: [ - this.attemptingFetchLocalizedStrings() ? "" : Localization.getLocalizedString("WebClipper.Label.UnsupportedBrowser") - ]} - ]} - ]} - ]} - ]} - ]} - ]} - ]}); - } -} - -Polyfills.init(); -let component = UnsupportedBrowserClass.componentize(); -m.mount(document.getElementById("clipperUIPlaceholder"), component); -export {component as UnsupportedBrowser} diff --git a/src/scripts/communicator/communicatorPassthrough.ts b/src/scripts/communicator/communicatorPassthrough.ts deleted file mode 100644 index ed0b1149..00000000 --- a/src/scripts/communicator/communicatorPassthrough.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {Communicator, CommDataPackage} from "./communicator"; -import {MessageHandler} from "./messageHandler"; - -/** - * Links two communicators together and after initialization, passes all communication from either to the other - * The general logic to do this is as follows: - * Initialize with handler1 - * After an initial response from handler1, start queueing messages from handler1, and start initializing with handler2 - * After an initial response from handler2, flush any queued messages, and pass-through the rest of the communications from either - * Note: The channel specified should be the same as the channel the other sides are using - */ -export class CommunicatorPassthrough { - constructor(messageHandler1: MessageHandler, messageHandler2: MessageHandler, channel?: string, errorHandler?: (e: Error) => void) { - let comm1 = new Communicator(messageHandler1, channel); - comm1.setErrorHandler(errorHandler); - comm1.onInitialized = () => { - // Queue up any messages passed until we can initialize with the other side - let queuedMessages: CommDataPackage[] = []; - comm1.handleDataPackage = (dataPackage: CommDataPackage) => { - queuedMessages.push(dataPackage); - }; - - let comm2 = new Communicator(messageHandler2, channel); - comm2.setErrorHandler(errorHandler); - comm2.onInitialized = () => { - comm1.onInitialized = undefined; - comm2.onInitialized = undefined; - - // flush any queued up messages - for (let dataPackage of queuedMessages) { - comm2.postMessage(dataPackage); - } - - // Have each side simply pass packages to the other side untouched - comm1.handleDataPackage = (dataPackage: CommDataPackage) => { - comm2.postMessage(dataPackage); - }; - comm2.handleDataPackage = (dataPackage: CommDataPackage) => { - comm1.postMessage(dataPackage); - }; - }; - }; - } -} diff --git a/src/scripts/communicator/iframeMessageHandler.ts b/src/scripts/communicator/iframeMessageHandler.ts deleted file mode 100644 index 8ad18a9a..00000000 --- a/src/scripts/communicator/iframeMessageHandler.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {MessageHandler} from "./messageHandler"; -import * as Log from "../logging/log"; - -// Communication manager class for handling message passing between windows -export class IFrameMessageHandler extends MessageHandler { - private getOtherWindow: () => Window; - - constructor(getOtherWindow: () => Window) { - super(); - - this.getOtherWindow = getOtherWindow; - - this.initMessageHandler(); - window.addEventListener("message", this.messageHandler); - } - - protected initMessageHandler() { - this.messageHandler = (event: MessageEvent) => { - this.onMessageReceived(event.data); - - // Since the message was correctly handled, we don't want any pre-established handlers getting called - if (event.stopPropagation) { - event.stopPropagation(); - } else { - event.cancelBubble = true; - } - }; - } - - public sendMessage(dataString: string) { - let otherWindow = this.getOtherWindow(); - otherWindow.postMessage(dataString, "*"); - } - - public tearDown() { - window.removeEventListener("message", this.messageHandler); - } -} diff --git a/src/scripts/communicator/inlineMessageHandler.ts b/src/scripts/communicator/inlineMessageHandler.ts deleted file mode 100644 index 6cf9b1e3..00000000 --- a/src/scripts/communicator/inlineMessageHandler.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {MessageHandler} from "./messageHandler"; - -export class InlineMessageHandler extends MessageHandler { - private otherSide: InlineMessageHandler; - constructor(otherSide?: InlineMessageHandler) { - super(); - this.setOtherSide(otherSide); - } - - protected initMessageHandler(): void { } - - public inlineMessage(data: string) { - this.onMessageReceived(data); - } - - public setOtherSide(otherSide: InlineMessageHandler) { - this.otherSide = otherSide; - } - - public sendMessage(data: string): void { - if (this.otherSide) { - this.otherSide.inlineMessage(data); - } - } - - public tearDown() { } -} diff --git a/src/scripts/communicator/offscreenCommunicator.ts b/src/scripts/communicator/offscreenCommunicator.ts index c5769afa..62d61ba7 100644 --- a/src/scripts/communicator/offscreenCommunicator.ts +++ b/src/scripts/communicator/offscreenCommunicator.ts @@ -1,8 +1,11 @@ -import {WebExtension} from "../extensions/webExtensionBase/webExtension"; import {OffscreenMessageTypes} from "./offscreenMessageTypes"; let creating: Promise; // A global promise to avoid concurrency issues +// Use chrome API directly — WebExtension.browser is only initialized in the +// service worker context, but this module is also imported by the clipper UI. +let offscreenUrl = chrome.runtime.getURL("offscreen.html"); + // This function performs basic filtering and error checking on messages before // dispatching the message to a more specific message handler. async function handleResponse(message): Promise { @@ -27,17 +30,21 @@ async function handleResponse(message): Promise { } export async function sendToOffscreenDocument(type: string, data: any): Promise { - const existingContexts = await WebExtension.browser.runtime.getContexts({ - contextTypes: [WebExtension.browser.runtime.ContextType.OFFSCREEN_DOCUMENT], - documentUrls: [WebExtension.offscreenUrl] + // Access newer Chrome APIs via runtime references to avoid hardcoded strings + let chromeRuntime = chrome.runtime as any; + let chromeOffscreen = (chrome as any).offscreen; + + const existingContexts = await chromeRuntime.getContexts({ + contextTypes: [chromeRuntime.ContextType.OFFSCREEN_DOCUMENT], + documentUrls: [offscreenUrl] }); if (creating) { await creating; } else if (existingContexts.length === 0) { - creating = WebExtension.browser.offscreen.createDocument({ - url: WebExtension.offscreenUrl, - reasons: [WebExtension.browser.offscreen.Reason.DOM_PARSER], + creating = chromeOffscreen.createDocument({ + url: offscreenUrl, + reasons: [chromeOffscreen.Reason.DOM_PARSER], justification: "Parse DOM", }); await creating; @@ -45,18 +52,12 @@ export async function sendToOffscreenDocument(type: string, data: any): Promise< } return new Promise(resolve => { - WebExtension.browser.runtime.sendMessage(JSON.stringify({ + chrome.runtime.sendMessage(JSON.stringify({ type: type, target: "offscreen", data: data }), (message) => { handleResponse(message).then((result) => { - /** - * Commenting out the following line in order to always keep 1 offscreen document open - * so as to avoid concurrency issues with multiple offscreen documents. - * TODO: Investigate if there is a better way to handle concurrency issues. - */ - // WebExtension.browser.offscreen.closeDocument(); resolve(result); }); }); diff --git a/src/scripts/constants.ts b/src/scripts/constants.ts index c862082e..331ba0ba 100644 --- a/src/scripts/constants.ts +++ b/src/scripts/constants.ts @@ -314,6 +314,11 @@ export module Constants { export var getMultipleStorageValues = "GET_MULTIPLE_STORAGE_VALUES"; export var getTooltipToRenderInPageNav = "GET_TOOLTIP_TO_RENDER_IN_PAGE_NAV"; export var hideUi = "HIDE_UI"; + export var showUi = "SHOW_UI"; + export var showSignInPanel = "SHOW_SIGN_IN_PANEL"; + export var startRegionCapture = "START_REGION_CAPTURE"; + export var regionCaptureComplete = "REGION_CAPTURE_COMPLETE"; + export var regionCaptureCancelled = "REGION_CAPTURE_CANCELLED"; export var invokeClipper = "INVOKE_CLIPPER"; export var invokeClipperFromPageNav = "INVOKE_CLIPPER_FROM_PAGE_NAV"; export var invokeDebugLogging = "INVOKE_DEBUG_LOGGING"; @@ -330,6 +335,8 @@ export module Constants { export var signOutUser = "SIGN_OUT_USER"; export var tabToLowestIndexedElement = "TAB_TO_LOWEST_INDEXED_ELEMENT"; export var takeTabScreenshot = "TAKE_TAB_SCREENSHOT"; + export var takeFullPageScreenshot = "TAKE_FULL_PAGE_SCREENSHOT"; + export var cancelFullPageScreenshot = "CANCEL_FULL_PAGE_SCREENSHOT"; export var telemetry = "TELEMETRY"; export var toggleClipper = "TOGGLE_CLIPPER"; export var unloadHandler = "UNLOAD_HANDLER"; @@ -382,9 +389,7 @@ export module Constants { export module Urls { export var serviceDomain = "https://www.onenote.com"; - export var augmentationApiUrl = serviceDomain + "/onaugmentation/clipperextract/v1.0/"; export var changelogUrl = serviceDomain + "/whatsnext/webclipper"; - export var fullPageScreenshotUrl = serviceDomain + "/onaugmentation/clipperDomEnhancer/v1.0/"; export var localizedStringsUrlBase = serviceDomain + "/strings?ids=WebClipper."; export var clipperInstallPageUrl = "https://support.microsoft.com/en-us/office/getting-started-with-the-onenote-web-clipper-5696609d-c5ae-4591-b3af-1f897cb6eda6"; diff --git a/src/scripts/contentCapture/augmentationHelper.ts b/src/scripts/contentCapture/augmentationHelper.ts deleted file mode 100644 index b5c7ee58..00000000 --- a/src/scripts/contentCapture/augmentationHelper.ts +++ /dev/null @@ -1,165 +0,0 @@ -import {Constants} from "../constants"; -import {Settings} from "../settings"; -import {StringUtils} from "../stringUtils"; -import {ObjectUtils} from "../objectUtils"; - -import {Clipper} from "../clipperUI/frontEndGlobals"; -import {ClipperState} from "../clipperUI/clipperState"; -import {OneNoteApiUtils} from "../clipperUI/oneNoteApiUtils"; - -import {DomUtils, EmbeddedVideoIFrameSrcs} from "../domParsers/domUtils"; - -import {HttpWithRetries} from "../http/httpWithRetries"; - -import * as Log from "../logging/log"; - -import {CaptureFailureInfo} from "./captureFailureInfo"; -import { ErrorUtils, ResponsePackage } from "../responsePackage"; - -export enum AugmentationModel { - None, - Article, - BizCard, - EntityKnowledge, - Recipe, - Product, - Screenshot, - Wrapstar -} - -export interface AugmentationResult extends CaptureFailureInfo { - ContentInHtml?: string; - ContentModel?: AugmentationModel; - ContentObjects?: any[]; - PageMetadata?: { [key: string]: string }; -} - -export class AugmentationHelper { - public static augmentPage(url: string, locale: string, pageContent: string): Promise { - return new Promise((resolve, reject) => { - let augmentationEvent = new Log.Event.PromiseEvent(Log.Event.Label.AugmentationApiCall); - - let correlationId = StringUtils.generateGuid(); - augmentationEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, correlationId); - - AugmentationHelper.makeAugmentationRequest(url, locale, pageContent, correlationId).then((responsePackage: { parsedResponse: AugmentationResult[], response: Response }) => { - let parsedResponse = responsePackage.parsedResponse; - let result: AugmentationResult = { ContentModel: AugmentationModel.None, ContentObjects: [] }; - - augmentationEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, responsePackage.response.headers.get(Constants.HeaderValues.correlationId)); - - if (parsedResponse && parsedResponse.length > 0 && parsedResponse[0].ContentInHtml) { - result = parsedResponse[0]; - - augmentationEvent.setCustomProperty(Log.PropertyName.Custom.AugmentationModel, AugmentationModel[result.ContentModel]); - - // Remove tags that are unsupported by ONML before we display them in the preview - // Supported tags: https://msdn.microsoft.com/en-us/library/office/dn575442.aspx - let doc = (new DOMParser()).parseFromString(result.ContentInHtml, "text/html"); - let previewElement = AugmentationHelper.getArticlePreviewElement(doc); - - DomUtils.toOnml(doc).then(async () => { - DomUtils.addPreviewContainerStyling(previewElement); - await AugmentationHelper.addSupportedVideosToElement(previewElement, pageContent, url); - result.ContentInHtml = doc.body.innerHTML; - resolve(result); - }); - } else { - resolve(result); - } - - augmentationEvent.setCustomProperty(Log.PropertyName.Custom.AugmentationModel, AugmentationModel[result.ContentModel]); - }).catch((failure: OneNoteApi.RequestError) => { - OneNoteApiUtils.logOneNoteApiRequestError(augmentationEvent, failure); - reject(); - }).then(() => { - Clipper.logger.logEvent(augmentationEvent); - }); - }); - } - - public static getAugmentationType(state: ClipperState): string { - // Default type to Article mode - let augmentationType: string = AugmentationModel[AugmentationModel.Article].toString(); - - if (!state || !state.augmentationResult || !state.augmentationResult.data) { - return augmentationType; - } - - // TODO: There is a work-item to change the AugmentationApi to return ContentModel as a StringUtils - // instead of an integer - let contentModel: AugmentationModel = state.augmentationResult.data.ContentModel; - - if (AugmentationHelper.isSupportedAugmentationType(contentModel)) { - augmentationType = AugmentationModel[contentModel].toString(); - } - - return augmentationType; - } - - /* - * Returns the augmented preview text. - */ - public static makeAugmentationRequest(url: string, locale: string, pageContent: string, requestCorrelationId: string): Promise> { - return new Promise>((resolve, reject) => { - Clipper.getUserSessionIdWhenDefined().then((sessionId) => { - let augmentationApiUrl = Constants.Urls.augmentationApiUrl + "?renderMethod=extractAggressive&url=" + url + "&lang=" + locale; - - let headers = {}; - headers[Constants.HeaderValues.appIdKey] = Settings.getSetting("App_Id"); - headers[Constants.HeaderValues.noAuthKey] = "true"; - headers[Constants.HeaderValues.correlationId] = requestCorrelationId; - headers[Constants.HeaderValues.userSessionIdKey] = sessionId; - - HttpWithRetries.post(augmentationApiUrl, pageContent, headers).then((response: Response) => { - response.text().then((responseText: string) => { - let parsedResponse: any; - try { - parsedResponse = JSON.parse(responseText); - } catch (e) { - Clipper.logger.logJsonParseUnexpected(responseText); - ErrorUtils.createRequestErrorObject(response, OneNoteApi.RequestErrorType.UNABLE_TO_PARSE_RESPONSE).then((error) => { - reject(error); - }); - } - - let responsePackage = { - parsedResponse: parsedResponse, - response: response - }; - resolve(responsePackage); - }); - }); - }); - }); - } - - public static getArticlePreviewElement(doc: Document): HTMLElement { - let mainContainers = doc.getElementsByClassName("MainArticleContainer"); - if (ObjectUtils.isNullOrUndefined(mainContainers) || ObjectUtils.isNullOrUndefined(mainContainers[0])) { - return doc.body; - } - return mainContainers[0] as HTMLElement; - } - - private static isSupportedAugmentationType(contentModel: number): boolean { - return contentModel === AugmentationModel.Article || - contentModel === AugmentationModel.Recipe || - contentModel === AugmentationModel.Product; - } - - private static addSupportedVideosToElement(previewElement: HTMLElement, pageContent: string, url: string): Promise { - let addEmbeddedVideoEvent = new Log.Event.PromiseEvent(Log.Event.Label.AddEmbeddedVideo); // start event timer, just in case it gets logged - - return DomUtils.addEmbeddedVideosWhereSupported(previewElement, pageContent, url).then((videoSrcUrls: EmbeddedVideoIFrameSrcs[]) => { - // only log when supported video is found on page - if (!ObjectUtils.isNullOrUndefined(videoSrcUrls)) { - Clipper.logger.logEvent(addEmbeddedVideoEvent); - } - }, (error: OneNoteApi.GenericError) => { - addEmbeddedVideoEvent.setStatus(Log.Status.Failed); - addEmbeddedVideoEvent.setFailureInfo(error); - Clipper.logger.logEvent(addEmbeddedVideoEvent); - }); - } -} diff --git a/src/scripts/contentCapture/bookmarkHelper.ts b/src/scripts/contentCapture/bookmarkHelper.ts deleted file mode 100644 index 7934480a..00000000 --- a/src/scripts/contentCapture/bookmarkHelper.ts +++ /dev/null @@ -1,295 +0,0 @@ -import {ObjectUtils} from "../objectUtils"; - -import {Clipper} from "../clipperUI/frontEndGlobals"; - -import {DomUtils} from "../domParsers/domUtils"; - -import * as Log from "../logging/log"; - -import {CaptureFailureInfo} from "./captureFailureInfo"; - -export interface BookmarkResult extends CaptureFailureInfo { - url: string; - title: string; - description?: string; - thumbnailSrc?: string; -} - -export interface BookmarkError extends OneNoteApi.GenericError { - url: string; - description?: string; - thumbnailSrc?: string; -} - -export interface MetadataKeyValuePair { - key: string; - value: string; -} - -export class BookmarkHelper { - public static maxNumCharsInDescription = 140; - - public static metadataTagNames: string[] = [ DomUtils.tags.meta, DomUtils.tags.link ]; - - public static nameAttrName = "name"; - public static propertyAttrName = "property"; - public static relAttrName = "rel"; - public static srcAttrName = "src"; - - public static primaryDescriptionKeyValuePair: MetadataKeyValuePair = { key: BookmarkHelper.propertyAttrName, value: "og:description" }; - public static primaryThumbnailKeyValuePair: MetadataKeyValuePair = { key: BookmarkHelper.propertyAttrName, value: "og:image" }; - - public static firstImageOnPageKeyValuePair: MetadataKeyValuePair = { key: "", value: "firstImageOnPage" }; - public static textOnPageKeyValuePair: MetadataKeyValuePair = { key: "", value: "textOnPage" }; - - // list is ordered by fallback priority - public static fallbackDescriptionKeyValuePairs: MetadataKeyValuePair[] = [ - { key: BookmarkHelper.nameAttrName, value: "description" }, - { key: BookmarkHelper.nameAttrName, value: "twitter:description" }, - { key: BookmarkHelper.nameAttrName, value: "keywords" }, - { key: BookmarkHelper.propertyAttrName, value: "article:tag" } - ]; - - // list is ordered by fallback priority - public static fallbackThumbnailKeyValuePairs: MetadataKeyValuePair[] = [ - { key: BookmarkHelper.nameAttrName, value: "twitter:image:src" }, - { key: BookmarkHelper.nameAttrName, value: "twitter:image" }, - { key: BookmarkHelper.relAttrName, value: "image_src" }, - { key: BookmarkHelper.relAttrName, value: "icon" } - ]; - - /** - * Grab useful metadata for a summary/bookmark view of a page from the provided DOM elements - * - * allowFallback: if true, we will attempt to infer bookmarking info from metadata - * that does not adhere to the Open Graph protocol (http://ogp.me/) - */ - public static bookmarkPage(url: string, pageTitle: string, metadataElements: Element[], allowFallback = false, imageElements?: HTMLImageElement[], textElements?: Text[]): Promise { - let bookmarkPageEvent = new Log.Event.PromiseEvent(Log.Event.Label.BookmarkPage); - - if (ObjectUtils.isNullOrUndefined(url) || url === "") { - let error: OneNoteApi.GenericError = { error: "Page url is null, undefined, or empty" }; - bookmarkPageEvent.setStatus(Log.Status.Failed); - bookmarkPageEvent.setFailureInfo(error); - - Clipper.logger.logEvent(bookmarkPageEvent); - return Promise.reject(error); - } - - let result: BookmarkResult = { - url: url, - title: pageTitle - }; - - let bookmarkLoggingInfo = { - metadataElementsExist: true, - pageTitleExists: true, - descriptionMetadataUsed: "", - thumbnailSrcMetadataUsed: "", - thumbnailSrcToDataUrlFailure: undefined - }; - - if (ObjectUtils.isNullOrUndefined(pageTitle) || pageTitle.length === 0) { - bookmarkLoggingInfo.pageTitleExists = false; - } - - if (ObjectUtils.isNullOrUndefined(metadataElements) || metadataElements.length === 0) { - bookmarkLoggingInfo.metadataElementsExist = false; - Clipper.logger.logEvent(bookmarkPageEvent); - return Promise.resolve(result); - } - - let descriptionResult = this.getPrimaryDescription(metadataElements); - if (allowFallback && ObjectUtils.isNullOrUndefined(descriptionResult)) { - descriptionResult = BookmarkHelper.getFallbackDescription(metadataElements); - - if (ObjectUtils.isNullOrUndefined(descriptionResult)) { - // concatenate text on the page if all else fails - descriptionResult = BookmarkHelper.getTextOnPage(textElements); - } - } - - let thumbnailSrcResult = this.getPrimaryThumbnailSrc(metadataElements); - if (allowFallback && ObjectUtils.isNullOrUndefined(thumbnailSrcResult)) { - thumbnailSrcResult = BookmarkHelper.getFallbackThumbnailSrc(metadataElements); - - if (ObjectUtils.isNullOrUndefined(thumbnailSrcResult)) { - // get first image on the page as thumbnail if all else fails - thumbnailSrcResult = BookmarkHelper.getFirstImageOnPage(imageElements); - } - } - - // populate final result object and log - - if (!ObjectUtils.isNullOrUndefined(descriptionResult)) { - descriptionResult.description = BookmarkHelper.truncateString(descriptionResult.description); - - result.description = descriptionResult.description; - bookmarkLoggingInfo.descriptionMetadataUsed = descriptionResult.metadataUsed.value; - } - - if (!ObjectUtils.isNullOrUndefined(thumbnailSrcResult)) { - bookmarkLoggingInfo.thumbnailSrcMetadataUsed = thumbnailSrcResult.metadataUsed.value; - - thumbnailSrcResult.thumbnailSrc = DomUtils.toAbsoluteUrl(thumbnailSrcResult.thumbnailSrc, url); - - return DomUtils.getImageDataUrl(thumbnailSrcResult.thumbnailSrc).then((thumbnailDataUrl: string) => { - if (!ObjectUtils.isNullOrUndefined(thumbnailDataUrl)) { - result.thumbnailSrc = thumbnailDataUrl; - } else { - result.thumbnailSrc = thumbnailSrcResult.thumbnailSrc; - bookmarkLoggingInfo.thumbnailSrcToDataUrlFailure = "thumbnail conversion to data url returned undefined. falling back to non-data url as source: " + thumbnailSrcResult.thumbnailSrc; - } - }).catch((error: OneNoteApi.GenericError) => { - bookmarkLoggingInfo.thumbnailSrcToDataUrlFailure = error.error; - }).then(() => { - Clipper.logger.logEvent(bookmarkPageEvent); - return Promise.resolve(result); - }); - } else { - Clipper.logger.logEvent(bookmarkPageEvent); - return Promise.resolve(result); - } - } - - /** - * Wrapper for native JS getElementsByTagName() which adds ability to provide multiple tag names - */ - public static getElementsByTagName(root: Document | Element, tagNames: string[]): Element[] { - if (ObjectUtils.isNullOrUndefined(root) || ObjectUtils.isNullOrUndefined(tagNames)) { - return; - } - - let elements: Element[] = new Array(); - for (let tag of tagNames) { - elements = elements.concat(elements, Array.prototype.slice.call(root.getElementsByTagName(tag))); - } - - return elements; - } - - /** - * Get non-whitespace text elements from the document - * - * cleanDoc: if true, THIS WILL MODIFY THE DOCUMENT PROVIDED - - * we will attempt to make the document ONML-friendly before grabbing text elements - */ - public static getNonWhiteSpaceTextElements(doc: Document, cleanDoc = false): Text[] { - if (cleanDoc) { - DomUtils.removeElementsNotSupportedInOnml(doc); - } - - return DomUtils.textNodesNoWhitespaceUnder(doc); - } - - public static getPrimaryDescription(metadataElements: Element[]): { metadataUsed: MetadataKeyValuePair, description: string } { - let metadata = BookmarkHelper.primaryDescriptionKeyValuePair; - let description = BookmarkHelper.getMetaContent(metadataElements, metadata); - - if (!ObjectUtils.isNullOrUndefined(description)) { - return { metadataUsed: metadata, description: description }; - } - } - - public static getFallbackDescription(metadataElements: Element[]): { metadataUsed: MetadataKeyValuePair, description: string } { - for (let metadata of BookmarkHelper.fallbackDescriptionKeyValuePairs) { - let description = BookmarkHelper.getMetaContent(metadataElements, metadata); - if (!ObjectUtils.isNullOrUndefined(description)) { - return { metadataUsed: metadata, description: description }; - } - } - } - - public static getTextOnPage(textElements: Text[], numberOfWords = 50): { metadataUsed: MetadataKeyValuePair, description: string } { - if (!ObjectUtils.isNullOrUndefined(textElements) && textElements.length > 0) { - let metadata = BookmarkHelper.textOnPageKeyValuePair; - let description = textElements.map(text => text.wholeText.trim()).join(" "); - if (!ObjectUtils.isNullOrUndefined(description) && description.length > 0) { - return { metadataUsed: metadata, description: description }; - } - } - } - - public static truncateString(longStr: string, numChars = BookmarkHelper.maxNumCharsInDescription): string { - if (ObjectUtils.isNullOrUndefined(longStr)) { - return; - } - - if (longStr.length === 0) { - return ""; - } - - if (longStr.length <= numChars) { - return longStr; - } - - let ellipsisAppend = "..."; - let truncateRegEx = new RegExp("^(.{" + numChars + "}[^\\s]*)"); - let truncateMatch = longStr.replace(/\s+/g, " ").match(truncateRegEx); - - if (ObjectUtils.isNullOrUndefined(truncateMatch)) { - return; - } - - return truncateMatch[1] + ellipsisAppend; - } - - public static getPrimaryThumbnailSrc(metaTags: Element[]): { metadataUsed: MetadataKeyValuePair, thumbnailSrc: string } { - let metadata = BookmarkHelper.primaryThumbnailKeyValuePair; - let imgSrc = BookmarkHelper.getMetaContent(metaTags, metadata); - - if (!ObjectUtils.isNullOrUndefined(imgSrc)) { - return { metadataUsed: metadata, thumbnailSrc: imgSrc }; - } - } - - public static getFallbackThumbnailSrc(metaTags: Element[]): { metadataUsed: MetadataKeyValuePair, thumbnailSrc: string } { - for (let metadata of BookmarkHelper.fallbackThumbnailKeyValuePairs) { - let imgSrc = BookmarkHelper.getMetaContent(metaTags, metadata); - if (!ObjectUtils.isNullOrUndefined(imgSrc)) { - return { metadataUsed: metadata, thumbnailSrc: imgSrc }; - } - } - } - - public static getFirstImageOnPage(imageElements: HTMLImageElement[]): { metadataUsed: MetadataKeyValuePair, thumbnailSrc: string } { - if (!ObjectUtils.isNullOrUndefined(imageElements) && imageElements.length > 0) { - let imgSrc = imageElements[0].getAttribute(BookmarkHelper.srcAttrName); - let metadata = BookmarkHelper.firstImageOnPageKeyValuePair; - if (!ObjectUtils.isNullOrUndefined(imgSrc) && imgSrc !== "") { - return { metadataUsed: metadata, thumbnailSrc: imgSrc }; - } - } - } - - public static getMetaContent(metaTags: Element[], metadata: MetadataKeyValuePair): string { - if (ObjectUtils.isNullOrUndefined(metaTags) || - ObjectUtils.isNullOrUndefined(metadata) || - ObjectUtils.isNullOrUndefined(metadata.key) || - ObjectUtils.isNullOrUndefined(metadata.value)) { - return; - } - - for (let tag of metaTags) { - let attributeValue = tag.getAttribute(metadata.key); - if (attributeValue && - attributeValue.toLowerCase().split(/\s/).indexOf(metadata.value.toLowerCase()) > -1) { - - let contentAttr: string; - if (tag.nodeName === "LINK") { - contentAttr = "href"; - } - if (tag.nodeName === "META") { - contentAttr = "content"; - } - - let content: string = tag.getAttribute(contentAttr); - - if (ObjectUtils.isNullOrUndefined(content) || content.length === 0) { - return; - } - return content; - } - } - } -} diff --git a/src/scripts/contentCapture/captureFailureInfo.ts b/src/scripts/contentCapture/captureFailureInfo.ts deleted file mode 100644 index b83f7338..00000000 --- a/src/scripts/contentCapture/captureFailureInfo.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Data object that represents error info to display to the user when - * something went wrong with content capture. - */ -export interface CaptureFailureInfo { - failureMessage?: string; -} diff --git a/src/scripts/contentCapture/fullPageScreenshotHelper.ts b/src/scripts/contentCapture/fullPageScreenshotHelper.ts deleted file mode 100644 index 505e65f4..00000000 --- a/src/scripts/contentCapture/fullPageScreenshotHelper.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {Clipper} from "../clipperUI/frontEndGlobals"; -import {OneNoteApiUtils} from "../clipperUI/oneNoteApiUtils"; - -import {HttpWithRetries} from "../http/httpWithRetries"; - -import * as Log from "../logging/log"; - -import {Constants} from "../constants"; -import {Settings} from "../settings"; -import {StringUtils} from "../stringUtils"; - -import {CaptureFailureInfo} from "./captureFailureInfo"; -import { ErrorUtils } from "../responsePackage"; - -export interface FullPageScreenshotResult extends CaptureFailureInfo { - ImageEncoding?: string; - ImageFormat?: string; - Images?: string[]; -} - -export class FullPageScreenshotHelper { - private static timeout = 50000; - - public static getFullPageScreenshot(pageInfoContentData: string): Promise { - return new Promise((resolve, reject) => { - Clipper.getUserSessionIdWhenDefined().then((sessionId) => { - let fullPageScreenshotEvent = new Log.Event.PromiseEvent(Log.Event.Label.FullPageScreenshotCall); - - let correlationId = StringUtils.generateGuid(); - fullPageScreenshotEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, correlationId); - - let headers = {}; - headers[Constants.HeaderValues.accept] = "application/json"; - headers[Constants.HeaderValues.appIdKey] = Settings.getSetting("App_Id"); - headers[Constants.HeaderValues.noAuthKey] = "true"; - headers[Constants.HeaderValues.correlationId] = correlationId; - headers[Constants.HeaderValues.userSessionIdKey] = sessionId; - - let errorCallback = (error: OneNoteApi.RequestError) => { - fullPageScreenshotEvent.setCustomProperty(Log.PropertyName.Custom.CorrelationId, error.responseHeaders[Constants.HeaderValues.correlationId]); - OneNoteApiUtils.logOneNoteApiRequestError(fullPageScreenshotEvent, error); - }; - - HttpWithRetries.post(Constants.Urls.fullPageScreenshotUrl, pageInfoContentData, headers, [200, 204], FullPageScreenshotHelper.timeout).then((response: Response) => { - if (response.status === 200) { - response.text().then((responseText: string) => { - try { - resolve(JSON.parse(responseText) as FullPageScreenshotResult); - fullPageScreenshotEvent.setCustomProperty(Log.PropertyName.Custom.FullPageScreenshotContentFound, true); - } catch (e) { - ErrorUtils.createRequestErrorObject(response, OneNoteApi.RequestErrorType.UNABLE_TO_PARSE_RESPONSE, FullPageScreenshotHelper.timeout).then((error) => { - reject(error); - }); - } - }); - } else { - fullPageScreenshotEvent.setCustomProperty(Log.PropertyName.Custom.FullPageScreenshotContentFound, false); - reject(); - } - }, (error: OneNoteApi.RequestError) => { - errorCallback(error); - reject(); - }).then(() => { - Clipper.logger.logEvent(fullPageScreenshotEvent); - }); - }); - }); - } -} diff --git a/src/scripts/contentCapture/pdfDocument.ts b/src/scripts/contentCapture/pdfDocument.ts deleted file mode 100644 index 8685ee3d..00000000 --- a/src/scripts/contentCapture/pdfDocument.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {ViewportDimensions} from "./viewportDimensions"; - -/** - * Adapter for implementations of pdf documents. All page numbers - * are 0-based indexes. - */ -export interface PdfDocument { - numPages(): number; - getByteLength(): Promise; - getData(): Promise; - getPageListAsDataUrls(pageIndexes: number[]): Promise; - getPageAsDataUrl(pageIndex: number): Promise; - getAllPageViewportDimensions(): Promise; - getPageListViewportDimensions(pageIndexes: number[]): Promise; - getPageViewportDimensions(pageIndex: number): Promise; -} diff --git a/src/scripts/contentCapture/pdfJsDocument.ts b/src/scripts/contentCapture/pdfJsDocument.ts deleted file mode 100644 index 8e68594d..00000000 --- a/src/scripts/contentCapture/pdfJsDocument.ts +++ /dev/null @@ -1,106 +0,0 @@ -import {ArrayUtils} from "../arrayUtils"; - -import {PdfDocument} from "./pdfDocument"; -import {ViewportDimensions} from "./viewportDimensions"; - -import * as _ from "lodash"; - -export class PdfJsDocument implements PdfDocument { - private pdf: PDFDocumentProxy; - - constructor(pdf: PDFDocumentProxy) { - this.pdf = pdf; - } - - public numPages(): number { - return this.pdf.numPages; - } - - public getByteLength(): Promise { - return this.getData().then((buffer) => { - return Promise.resolve(buffer.length); - }); - } - - public getData(): Promise { - return new Promise((resolve) => { - this.pdf.getData().then((buffer) => { - resolve(buffer); - }); - }); - } - - public getPageListAsDataUrls(pageIndexes: number[]): Promise { - let dataUrls: string[] = new Array(pageIndexes.length); - return new Promise((resolve) => { - for (let i = 0; i < pageIndexes.length; i++) { - this.getPageAsDataUrl(pageIndexes[i]).then((dataUrl) => { - dataUrls[i] = dataUrl; - if (ArrayUtils.isArrayComplete(dataUrls)) { - resolve(dataUrls); - } - }); - } - }); - } - - public getPageAsDataUrl(pageIndex: number): Promise { - return new Promise((resolve) => { - // In pdf.js, indexes start at 1 - this.pdf.getPage(pageIndex + 1).then((page) => { - // Issue #369: When the scale is set to 1, we end up with poor quality images - // on high resolution machines. The best explanation I found for this was in - // this article: http://stackoverflow.com/questions/35400722/pdf-image-quality-is-bad-using-pdf-js - // Note that we played around with setting the scale from 3-5, which looked better on high - // resolution monitors - but did not look good at all at lower resolutions. scale=2 seems - // to be the happy medium. - let viewport = page.getViewport(2 /* scale */); - let canvas = document.createElement("canvas") as HTMLCanvasElement; - let context = canvas.getContext("2d"); - canvas.height = viewport.height; - canvas.width = viewport.width; - - let renderContext = { - canvasContext: context, - viewport: viewport - }; - - page.render(renderContext).then(() => { - resolve(canvas.toDataURL()); - }); - }); - }); - } - - public getAllPageViewportDimensions(): Promise { - let allPageIndexes = _.range(this.numPages()); - return this.getPageListViewportDimensions(allPageIndexes); - } - - public getPageListViewportDimensions(pageIndexes: number[]): Promise { - let dimensions: ViewportDimensions[] = new Array(pageIndexes.length); - return new Promise((resolve) => { - for (let i = 0; i < pageIndexes.length; i++) { - this.getPageViewportDimensions(pageIndexes[i]).then((dimension) => { - dimensions[i] = dimension; - if (ArrayUtils.isArrayComplete(dimensions)) { - resolve(dimensions); - } - }); - } - }); - } - - public getPageViewportDimensions(pageIndex: number): Promise { - return new Promise((resolve) => { - // In pdf.js, indexes start at 1 - this.pdf.getPage(pageIndex + 1).then((page) => { - let viewport = page.getViewport(1 /* scale */); - resolve({ - height: viewport.height, - width: viewport.width - }); - }); - }); - } -} diff --git a/src/scripts/contentCapture/pdfScreenshotHelper.ts b/src/scripts/contentCapture/pdfScreenshotHelper.ts deleted file mode 100644 index 27535ed6..00000000 --- a/src/scripts/contentCapture/pdfScreenshotHelper.ts +++ /dev/null @@ -1,102 +0,0 @@ -import {Clipper} from "../clipperUI/frontEndGlobals"; -import {Status} from "../clipperUI/status"; - -import {SmartValue} from "../communicator/smartValue"; - -import * as Log from "../logging/log"; - -import {ArrayUtils} from "../arrayUtils"; -import {Constants} from "../constants"; -import {PageInfo} from "../pageInfo"; - -import {CaptureFailureInfo} from "./captureFailureInfo"; -import {PdfDocument} from "./pdfDocument"; -import {PdfJsDocument} from "./pdfJsDocument"; -import {ViewportDimensions} from "./viewportDimensions"; -import { ErrorUtils } from "../responsePackage"; - -export interface PdfScreenshotResult extends CaptureFailureInfo { - pdf?: PdfDocument; - viewportDimensions?: ViewportDimensions[]; - byteLength?: number; -} - -export class PdfScreenshotHelper { - public static getLocalPdfData(localUrl: string): Promise { - return PdfScreenshotHelper.getPdfScreenshotResult(localUrl); - } - - public static getPdfData(url: string): Promise { - return new Promise((resolve, reject) => { - let getBinaryEvent = new Log.Event.PromiseEvent(Log.Event.Label.GetBinaryRequest); - - let errorCallback = (failureInfo: Promise) => { - failureInfo.then((error) => { - getBinaryEvent.setStatus(Log.Status.Failed); - error.response = error.responseHeaders = undefined; - getBinaryEvent.setFailureInfo(error); - Clipper.logger.logEvent(getBinaryEvent); - reject(); - }); - }; - - fetch(url) - .then(response => { - if (!response.ok) { - errorCallback(ErrorUtils.createRequestErrorObject(response, OneNoteApi.RequestErrorType.UNEXPECTED_RESPONSE_STATUS)); - } - return response.arrayBuffer(); - }) - .then(arrayBuffer => { - getBinaryEvent.setCustomProperty(Log.PropertyName.Custom.ByteLength, arrayBuffer.byteLength); - Clipper.logger.logEvent(getBinaryEvent); - - PdfScreenshotHelper.getPdfScreenshotResult(new Uint8Array(arrayBuffer)) - .then(pdfScreenshotResult => { - pdfScreenshotResult.byteLength = arrayBuffer.byteLength; - resolve(pdfScreenshotResult); - }); - }) - .catch(() => { - reject(OneNoteApi.RequestErrorType.NETWORK_ERROR); - }); - setTimeout(() => { - reject(OneNoteApi.RequestErrorType.REQUEST_TIMED_OUT); - }, 60000); - }); - } - - /** - * Source can be a buffer object, or a url (including local) - */ - private static getPdfScreenshotResult(source: string | Uint8Array): Promise { - // Never rejects, interesting - return new Promise((resolve, reject) => { - let pdfPromise: PDFPromise; - /** - * PDFJS.getDocument accepts either a string or a Uint8Array or a PDFSource as its source parameter. - * With the latest version of typescript, the type of the source parameter cannot be string | Uint8Array - * but must be exactly one of the three types mentioned above. The if-else block below ensures the same. - */ - if (typeof source === "string") { - // source is of type string and matches the corresponding overload of PDFJS.getDocument - pdfPromise = PDFJS.getDocument(source); - } else { - // source is of type Uint8Array and matches the corresponding overload of PDFJS.getDocument - pdfPromise = PDFJS.getDocument(source); - } - pdfPromise.then((pdf) => { - let pdfDocument: PdfDocument = new PdfJsDocument(pdf); - pdfDocument.getAllPageViewportDimensions().then((viewportDimensions) => { - pdfDocument.getByteLength().then((byteLength) => { - resolve({ - pdf: pdfDocument, - viewportDimensions: viewportDimensions, - byteLength: byteLength - }); - }); - }); - }); - }); - } -} diff --git a/src/scripts/contentCapture/readability.d.ts b/src/scripts/contentCapture/readability.d.ts new file mode 100644 index 00000000..54a6a896 --- /dev/null +++ b/src/scripts/contentCapture/readability.d.ts @@ -0,0 +1,30 @@ +declare module "@mozilla/readability" { + export class Readability { + constructor(doc: Document, options?: { + debug?: boolean; + maxElemsToParse?: number; + nbTopCandidates?: number; + charThreshold?: number; + classesToPreserve?: string[]; + keepClasses?: boolean; + }); + parse(): { + title: string; + content: string; + textContent: string; + length: number; + excerpt: string; + byline: string; + dir: string; + siteName: string; + lang: string; + publishedTime: string; + } | null; + } + + export function isProbablyReaderable(doc: Document, options?: { + minContentLength?: number; + minScore?: number; + visibilityChecker?: (node: Element) => boolean; + }): boolean; +} diff --git a/src/scripts/contentCapture/viewportDimensions.ts b/src/scripts/contentCapture/viewportDimensions.ts deleted file mode 100644 index 807a00e6..00000000 --- a/src/scripts/contentCapture/viewportDimensions.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ViewportDimensions { - height: number; - width: number; -} diff --git a/src/scripts/cookieUtils.ts b/src/scripts/cookieUtils.ts deleted file mode 100644 index 9efb1398..00000000 --- a/src/scripts/cookieUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {ObjectUtils} from "./objectUtils"; - -export class CookieUtils { - public static readCookie(cookieName: string, doc?: Document) { - if (ObjectUtils.isNullOrUndefined(doc)) { - doc = document; - } - - let cookieKVPairs: string[][] = document.cookie.split(";").map(kvPair => kvPair.split("=")); - - for (let cookie of cookieKVPairs) { - if (cookie[0].trim() === cookieName) { - return cookie[1].trim(); - } - } - } -} diff --git a/src/scripts/domParsers/domUtils.ts b/src/scripts/domParsers/domUtils.ts index 7856be96..21492645 100644 --- a/src/scripts/domParsers/domUtils.ts +++ b/src/scripts/domParsers/domUtils.ts @@ -336,6 +336,8 @@ export class DomUtils { */ public static getCleanDomOfCurrentPage(originalDoc: Document): string { let doc = DomUtils.cloneDocument(originalDoc); + DomUtils.inlineHiddenElements(doc, originalDoc); + DomUtils.flattenShadowDomSlots(doc, originalDoc); DomUtils.convertCanvasElementsToImages(doc, originalDoc); DomUtils.addBaseTagIfNecessary(doc, originalDoc.location); @@ -467,6 +469,89 @@ export class DomUtils { return container.insertBefore(spacerNode, referenceNode); } + /** + * Handle elements that were inside web components with shadow DOM. + * cloneNode(true) does NOT clone declarative shadow roots, so slotted content + * (e.g., dropdown panels with slot="dropdown") becomes visible as regular DOM. + * For elements whose shadow-hosted parent hid them via slot CSS, we check the + * original document's computed visibility and inline display:none if hidden. + * + * Also removes