/** * Silex, live web creation * http://projects.silexlabs.org/?/silex/ * * Copyright (c) 2012 Silex Labs * http://www.silexlabs.org/ * * Silex is available under the GPL license * http://www.silexlabs.org/silex/silex-licensing/ */ /** * @fileoverview * This class represents a File opened by Silex, * which is rendered by the Stage class * It has methods to manipulate the File */ import { Property } from '../model/property'; import { CloudStorage } from '../service/cloud-storage'; import { FileInfo, Model, View } from '../types'; import { getUiElements } from '../view/UiElements'; /** * @param model model class which holds the other models * @param view view class which holds the other views */ export class File { /** * max number of items in recent files */ static MAX_RECENT_FILES = 5; /** * loading css class */ static LOADING_CSS_CLASS = 'loading-website'; /** * loading css class */ static LOADING_LIGHT_CSS_CLASS = 'loading-website-light'; /** * current file url and path and info returned by CE * if the current file is a new file, it has no FileInfo * if set, this is an absolute URL, use silex.model.File::getFileInfo to get * the relatvie URL */ fileInfo: FileInfo = null; /** * true if the opened file is a template * this means that one must "save as" and not "save" */ isTemplate: boolean = false; // store the iframe window and document /** * the iframe element */ private iFrameElement_: HTMLIFrameElement; /** * iframe document */ private contentDocument_: Document; /** * iframe window */ private contentWindow_: Window; constructor(public model: Model, public view: View) { this.iFrameElement_ = getUiElements().stage; this.contentDocument_ = this.iFrameElement_.contentDocument; this.contentWindow_ = this.iFrameElement_.contentWindow; // reset iframe content // this is needed since iframes can keep their content // after a refresh in firefox this.contentDocument_.open(); this.getContentDocument().write(''); this.contentDocument_.close(); } /** * get the iframe document */ getContentDocument(): Document { return this.contentDocument_; } /** * get the iframe window */ getContentWindow(): Window { return this.contentWindow_; } /** * @return true if a website is being edited */ hasContent(): boolean { return !!this.contentDocument_.body && this.contentDocument_.body.childNodes.length > 0; } /** * build the html content * Parse the raw html and fill the bodyElement and headElement * @export */ setHtml( rawHtml: string, opt_cbk?: (() => any), opt_showLoader?: boolean) { // reset iframe content this.view.stageWrapper.cleanup(); this.contentDocument_.open(); this.getContentDocument().write(''); this.contentDocument_.close(); // loading if (opt_showLoader !== false) { getUiElements().stage.classList.add(File.LOADING_CSS_CLASS); } else { getUiElements().stage.classList.add(File.LOADING_LIGHT_CSS_CLASS); } // write the content this.contentDocument_.open(); this.contentDocument_.write(rawHtml); this.contentDocument_.close(); this.contentChanged(opt_cbk); } /** * the content of the iframe changed */ contentChanged(opt_cbk?: (() => any)) { // wait for the webste to be loaded // can not rely on the load event of the iframe because there may be missing // assets this.contentDocument_ = this.iFrameElement_.contentDocument; this.contentWindow_ = this.iFrameElement_.contentWindow; // tslint:disable:no-string-literal if (this.contentDocument_.body === null || this.contentWindow_ === null || this.contentWindow_['jQuery'] === null) { setTimeout(() => { this.contentChanged(opt_cbk); }, 10); return; } // check the integrity and store silex style sheet which holds silex // elements styles this.model.property.initStyles(this.contentDocument_); this.model.property.loadProperties(this.contentDocument_); this.model.component.initStyles(this.contentDocument_); // select the body this.model.body.emptySelection(); // update the settings this.model.head.updateFromDom(); // notify the caller if (opt_cbk) { opt_cbk(); } // remove publication path for templates if (this.isTemplate) { this.model.head.setPublicationPath(null); } setTimeout(() => { // restore the stage this.view.stageWrapper.init(this.iFrameElement_); // refresh the view const page = this.model.page.getCurrentPage(); this.model.page.setCurrentPage(page); // loading getUiElements().stage.classList.remove(File.LOADING_CSS_CLASS); getUiElements().stage.classList.remove(File.LOADING_LIGHT_CSS_CLASS); }, 10); } /** * build a string of the raw html content * remove all internal objects and attributes */ getHtml() { const generator = this.getHtmlGenerator(); let res = null; do { res = generator.next(); } while (!res.done); return res.value; } /** * async verion of getHtml * this is an optimisation needed to speedup drag start (which creates an undo * point) it uses generator to lower the load induced by these operations */ getHtmlAsync(cbk) { const generator = this.getHtmlGenerator(); this.getHtmlNextStep(cbk, generator); } /** * does one more step of the async getHtml process */ getHtmlNextStep(cbk, generator) { const res = generator.next(); if (res.done) { setTimeout(() => cbk(res.value), 0); } else { setTimeout(() => this.getHtmlNextStep(cbk, generator), 0); } } /** * the async getHtml process * yield after each step * FIXME: we should be able to avoid creating an alternative dom and handle * everything on the server side */ * getHtmlGenerator() { // update style tag (the dom do not update automatically when we change // document.styleSheets) const updatedStyles = this.model.property.getAllStyles(this.contentDocument_); this.model.property.saveProperties(this.contentDocument_); // clone const cleanFile = (this.contentDocument_.cloneNode(true) as Document); yield; // apply styles in JSON to the DOM, this is to ensure we save the styles // untuched by the browser const styleTag = cleanFile.querySelector('.' + Property.INLINE_STYLE_TAG_CLASS_NAME); styleTag.innerHTML = updatedStyles; yield; // get html this.model.body.removeWysihtmlMarkup(cleanFile); yield; let rawHtml = (cleanFile as Document).documentElement.outerHTML; yield; // add doctype rawHtml = '' + rawHtml; return rawHtml; } /** * load an arbitrary url as a silex html file * will not be able to save * @export */ openFromUrl( url: string, opt_cbk: ((p1: string) => any) = null, opt_errCbk: ((p1: any, p2: string) => any) = null) { this.isTemplate = true; CloudStorage.getInstance().loadLocal( url, (rawHtml, userHead) => { this.fileInfo = ({isDir: false, mime: 'text/html', url} as FileInfo); this.model.head.setUserHeadTag(userHead); if (opt_cbk) { opt_cbk(rawHtml); } }, opt_errCbk); } /** * save a file with a new name * @param cbk receives the raw HTML * @export */ saveAs(fileInfo: FileInfo, rawHtml: string, cbk: () => any, opt_errCbk?: ((p1: any, p2: string) => any)) { // save the data this.fileInfo = fileInfo; this.addToLatestFiles(this.fileInfo); this.save(rawHtml, cbk, opt_errCbk); } /** * write content to the file * @export */ save(rawHtml: string, cbk: () => any, opt_errCbk?: ((p1: any, p2: string) => any)) { if (this.fileInfo == null) { throw new Error('Can not save, fileInfo is null'); } CloudStorage.getInstance().write( (this.fileInfo as FileInfo), rawHtml, this.model.head.getUserHeadTag(), () => { this.isTemplate = false; if (cbk) { cbk(); } }, opt_errCbk); } /** * load a new file * @param cbk receives the raw HTML */ open( fileInfo: FileInfo, cbk: (p1: string) => any, opt_errCbk?: ((p1: any, p2: string) => any)) { this.isTemplate = false; CloudStorage.getInstance().read( fileInfo, (rawHtml, userHead) => { // update model this.close(); this.fileInfo = fileInfo; this.addToLatestFiles(this.fileInfo); this.model.head.setUserHeadTag(userHead); if (cbk) { cbk(rawHtml); } }, opt_errCbk); } /** * reset data, close file */ close() { this.fileInfo = null; } /** * get the url of the file */ getFileInfo(): FileInfo { return this.fileInfo; } /** * clear the recent files */ clearLatestFiles() { window.localStorage.removeItem('silex:recent-files'); } /** * get the latest opened files */ getLatestFiles(): FileInfo[] { const str = window.localStorage.getItem('silex:recent-files'); if (str) { return (JSON.parse(str) as FileInfo[]); } else { return []; } } /** * store this file in the latest opened files */ addToLatestFiles(fileInfo: FileInfo) { const latestFiles = [fileInfo].concat(this.getLatestFiles().filter( (item, idx) => item.absPath !== fileInfo.absPath && idx < File.MAX_RECENT_FILES)); window.localStorage.setItem( 'silex:recent-files', JSON.stringify(latestFiles)); } }